mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
@@ -40,19 +40,4 @@ describe('mentionsStore', () =>
|
||||
expect(getUnreadCount()).toBe(1);
|
||||
expect(getMentionsSnapshot().find(m => m.mentionId === 1)!.read).toBe(true);
|
||||
});
|
||||
|
||||
it('drops mentions with non-positive id (defensive against id=0 spam)', () =>
|
||||
{
|
||||
addMention(make(0));
|
||||
addMention(make(-1));
|
||||
expect(getMentionsSnapshot()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('dedupes duplicate ids even after the legacy id !== 0 carve-out is gone', () =>
|
||||
{
|
||||
addMention(make(7));
|
||||
addMention(make(7));
|
||||
addMention(make(7));
|
||||
expect(getMentionsSnapshot()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './useMentionsSnapshot';
|
||||
export * from './useMentionMessages';
|
||||
export * from './useMentionAutocomplete';
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
import { IMentionEntry } from '../../api/mentions';
|
||||
|
||||
// Hard cap on how many mentions we hold in memory at once. The server's
|
||||
// initial list is capped (mentions.store.limit, default 50) but live
|
||||
// MentionReceived packets feed into addMention unbounded - so a server bug
|
||||
// or a hostile/injected stream could otherwise grow the array and the DOM
|
||||
// forever. 200 is comfortably more than any realistic active user has and
|
||||
// well below anything that would inflate memory.
|
||||
const MAX_MENTIONS = 200;
|
||||
|
||||
let mentions: IMentionEntry[] = [];
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
const emit = () => { for(const l of listeners) l(); };
|
||||
|
||||
const cap = (list: IMentionEntry[]): IMentionEntry[] =>
|
||||
(list.length > MAX_MENTIONS) ? list.slice(0, MAX_MENTIONS) : list;
|
||||
|
||||
export const subscribeMentions = (onChange: () => void): (() => void) =>
|
||||
{
|
||||
listeners.add(onChange);
|
||||
@@ -28,21 +17,14 @@ export const getUnreadCount = (): number => mentions.reduce((n, m) => n + (m.rea
|
||||
|
||||
export const setMentions = (list: IMentionEntry[]): void =>
|
||||
{
|
||||
mentions = cap([...list].sort((a, b) => b.mentionId - a.mentionId));
|
||||
mentions = [...list].sort((a, b) => b.mentionId - a.mentionId);
|
||||
emit();
|
||||
};
|
||||
|
||||
export const addMention = (entry: IMentionEntry): void =>
|
||||
{
|
||||
// Drop entries the server failed to persist (generatedId 0 / negative).
|
||||
// The server hardening already refuses to push these, but the client
|
||||
// stays defensive in case a stale gameserver or an injected packet sends
|
||||
// one - without this guard, the old "id !== 0" dedup carve-out let
|
||||
// every duplicate through.
|
||||
if(!entry || !Number.isFinite(entry.mentionId) || entry.mentionId <= 0) return;
|
||||
if(mentions.some(m => m.mentionId === entry.mentionId)) return;
|
||||
|
||||
mentions = cap([entry, ...mentions]);
|
||||
if(mentions.some(m => m.mentionId === entry.mentionId && entry.mentionId !== 0)) return;
|
||||
mentions = [entry, ...mentions];
|
||||
emit();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,29 +1,15 @@
|
||||
import { MentionReceivedEvent, MentionsListEvent, RequestMentionsComposer } from '@nitrots/nitro-renderer';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { GetConfigurationValue, IMentionEntry, LocalizeText, NotificationBubbleType, PlaySound, SendMessageComposer } from '../../api';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { GetConfigurationValue, IMentionEntry, PlaySound, SendMessageComposer } from '../../api';
|
||||
import { useMessageEvent } from '../events';
|
||||
import { useNotificationActions } from '../notification';
|
||||
import { addMention, setMentions } from './mentionsStore';
|
||||
import { pushMentionToast } from './mentionToastsStore';
|
||||
|
||||
// Dedicated mention chime served from nitro-assets/sounds/<sample>.mp3.
|
||||
const MENTION_SOUND_SAMPLE = 'mentions_notification';
|
||||
|
||||
// Floor on the gap between bubble/chime notifications. Even if the server
|
||||
// (or an injected packet stream) pushes mentions faster than this, the user
|
||||
// gets at most one chime + bubble per window. The mentions list itself
|
||||
// still updates in real time - this only throttles the in-screen feedback.
|
||||
const NOTIFICATION_THROTTLE_MS = 1500;
|
||||
// Drop any single mention packet whose mention id we've already seen this
|
||||
// session, so a replay attack can't re-trigger the bubble + sound even if
|
||||
// the client store dropped the entry already.
|
||||
const SEEN_IDS_MAX = 500;
|
||||
|
||||
export const useMentionMessages = (): void =>
|
||||
{
|
||||
const { showSingleBubble } = useNotificationActions();
|
||||
const lastNotificationRef = useRef<number>(0);
|
||||
const seenIdsRef = useRef<Set<number>>(new Set());
|
||||
|
||||
const onMentionsList = useCallback((event: MentionsListEvent) =>
|
||||
{
|
||||
const list = event.getParser().mentions;
|
||||
@@ -32,7 +18,7 @@ export const useMentionMessages = (): void =>
|
||||
mentionId: m.mentionId,
|
||||
senderId: m.senderId,
|
||||
senderUsername: m.senderUsername,
|
||||
senderFigure: m.senderFigure ?? '',
|
||||
senderFigure: m.senderFigure,
|
||||
roomId: m.roomId,
|
||||
roomName: m.roomName,
|
||||
message: m.message,
|
||||
@@ -48,22 +34,11 @@ export const useMentionMessages = (): void =>
|
||||
|
||||
const m = event.getParser().mention;
|
||||
|
||||
if(!m || !Number.isFinite(m.mentionId) || m.mentionId <= 0) return;
|
||||
|
||||
const seen = seenIdsRef.current;
|
||||
if(seen.has(m.mentionId)) return;
|
||||
seen.add(m.mentionId);
|
||||
if(seen.size > SEEN_IDS_MAX)
|
||||
{
|
||||
const first = seen.values().next().value as number | undefined;
|
||||
if(first !== undefined) seen.delete(first);
|
||||
}
|
||||
|
||||
const entry: IMentionEntry = {
|
||||
mentionId: m.mentionId,
|
||||
senderId: m.senderId,
|
||||
senderUsername: m.senderUsername,
|
||||
senderFigure: m.senderFigure ?? '',
|
||||
senderFigure: m.senderFigure,
|
||||
roomId: m.roomId,
|
||||
roomName: m.roomName,
|
||||
message: m.message,
|
||||
@@ -74,20 +49,11 @@ export const useMentionMessages = (): void =>
|
||||
|
||||
addMention(entry);
|
||||
|
||||
const now = Date.now();
|
||||
if((now - lastNotificationRef.current) < NOTIFICATION_THROTTLE_MS) return;
|
||||
lastNotificationRef.current = now;
|
||||
|
||||
if(GetConfigurationValue<boolean>('mentions_ui.sound', true)) PlaySound(MENTION_SOUND_SAMPLE);
|
||||
|
||||
showSingleBubble(
|
||||
LocalizeText('mentions.notification', [ 'sender', 'room' ], [ entry.senderUsername, entry.roomName ]),
|
||||
NotificationBubbleType.INFO,
|
||||
null,
|
||||
'mentions/toggle',
|
||||
entry.senderUsername
|
||||
);
|
||||
}, [ showSingleBubble ]);
|
||||
// Notifica laterale custom (avatar + messaggio + dismiss) invece del bubble generico.
|
||||
pushMentionToast(entry);
|
||||
}, []);
|
||||
|
||||
useMessageEvent<MentionsListEvent>(MentionsListEvent, onMentionsList);
|
||||
useMessageEvent<MentionReceivedEvent>(MentionReceivedEvent, onMentionReceived);
|
||||
|
||||
Reference in New Issue
Block a user