diff --git a/src/api/mentions/IMentionEntry.ts b/src/api/mentions/IMentionEntry.ts index c8b89b3..99eebdf 100644 --- a/src/api/mentions/IMentionEntry.ts +++ b/src/api/mentions/IMentionEntry.ts @@ -3,6 +3,7 @@ export interface IMentionEntry mentionId: number; senderId: number; senderUsername: string; + senderFigure: string; roomId: number; roomName: string; message: string; diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index caa04ab..32eb535 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -48,7 +48,7 @@ import { UserAccountSettingsView } from './user-settings/UserAccountSettingsView import { UserSettingsView } from './user-settings/UserSettingsView'; import { WiredView } from './wired/WiredView'; import { WiredCreatorToolsView } from './wired-tools/WiredCreatorToolsView'; -import { MentionsView } from './mentions'; +import { MentionsView, MentionToastsView } from './mentions'; export const MainView: FC<{}> = props => { @@ -242,6 +242,8 @@ export const MainView: FC<{}> = props => { GetConfigurationValue('radio_ui.enabled', false) && } { (GetConfigurationValue('mentions_ui.enabled', true) && mentionsVisible) && setMentionsVisible(false) } /> } + { GetConfigurationValue('mentions_ui.enabled', true) && + } ); diff --git a/src/components/mentions/MentionToastsView.tsx b/src/components/mentions/MentionToastsView.tsx new file mode 100644 index 0000000..6943986 --- /dev/null +++ b/src/components/mentions/MentionToastsView.tsx @@ -0,0 +1,63 @@ +import { CreateLinkEvent, MarkMentionsReadComposer } from '@nitrots/nitro-renderer'; +import { FC, MouseEvent, useEffect } from 'react'; +import { FaTimes } from 'react-icons/fa'; +import { LocalizeText, SendMessageComposer } from '../../api'; +import { LayoutAvatarImageView } from '../../common'; +import { useExternalSnapshot } from '../../hooks/events/useExternalSnapshot'; +import { markRead } from '../../hooks/mentions/mentionsStore'; +import { dismissMentionToast, getMentionToasts, MentionToast, subscribeMentionToasts } from '../../hooks/mentions/mentionToastsStore'; + +// Quanto resta visibile un toast prima di nascondersi da solo (resta non-letto). +const AUTO_DISMISS_MS = 8000; + +const MentionToastItemView: FC<{ toast: MentionToast }> = ({ toast }) => +{ + useEffect(() => + { + const timer = window.setTimeout(() => dismissMentionToast(toast.mentionId), AUTO_DISMISS_MS); + return () => window.clearTimeout(timer); + }, [ toast.mentionId ]); + + // Dismiss esplicito: segna letta (badge toolbar si aggiorna) + persiste sul server + chiude. + const onDismiss = (event: MouseEvent) => + { + event.stopPropagation(); + markRead(toast.mentionId); + SendMessageComposer(new MarkMentionsReadComposer(1, toast.mentionId)); + dismissMentionToast(toast.mentionId); + }; + + const onOpen = () => + { + CreateLinkEvent('mentions/toggle'); + dismissMentionToast(toast.mentionId); + }; + + return ( +
+
+ +
+
+
{ toast.senderUsername }
+
{ toast.message }
+
+ +
+ ); +}; + +export const MentionToastsView: FC = () => +{ + const toasts = useExternalSnapshot(subscribeMentionToasts, getMentionToasts); + + if(!toasts || !toasts.length) return null; + + return ( +
+ { toasts.map(toast => ) } +
+ ); +}; diff --git a/src/components/mentions/index.ts b/src/components/mentions/index.ts index 5eaab12..8c125c1 100644 --- a/src/components/mentions/index.ts +++ b/src/components/mentions/index.ts @@ -1,5 +1,6 @@ export * from './MentionMessageView'; export * from './MentionRowView'; export * from './MentionsView'; +export * from './MentionToastsView'; export * from './mentionsFormat'; export * from './useMentionActions'; diff --git a/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx b/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx new file mode 100644 index 0000000..f59c040 --- /dev/null +++ b/src/components/room/widgets/chat-input/ChatInputMentionSelectorView.tsx @@ -0,0 +1,46 @@ +import { FC, useEffect, useRef } from 'react'; +import { LayoutAvatarImageView } from '../../../../common'; +import { MentionSuggestion } from '../../../../hooks/mentions/useMentionAutocomplete'; + +interface ChatInputMentionSelectorViewProps +{ + suggestions: MentionSuggestion[]; + selectedIndex: number; + onSelect: (suggestion: MentionSuggestion) => void; + onHover: (index: number) => void; +} + +export const ChatInputMentionSelectorView: FC = props => +{ + const { suggestions = [], selectedIndex = 0, onSelect = null, onHover = null } = props; + const listRef = useRef(null); + + useEffect(() => + { + if(!listRef.current) return; + + const selected = listRef.current.children[selectedIndex] as HTMLElement; + + if(selected) selected.scrollIntoView({ block: 'nearest' }); + }, [ selectedIndex ]); + + return ( +
+ { suggestions.map((suggestion, index) => ( +
onSelect(suggestion) } + onMouseEnter={ () => onHover(index) } + > +
+ { suggestion.isAlias + ? @ + : } +
+ @{ suggestion.name } +
+ )) } +
+ ); +}; diff --git a/src/components/room/widgets/chat-input/ChatInputView.tsx b/src/components/room/widgets/chat-input/ChatInputView.tsx index 9aa9491..dcb368a 100644 --- a/src/components/room/widgets/chat-input/ChatInputView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputView.tsx @@ -3,9 +3,10 @@ import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { ChatMessageTypeEnum, GetClubMemberLevel, GetConfigurationValue, LocalizeText, RoomWidgetUpdateChatInputContentEvent } from '../../../../api'; import { Text } from '../../../../common'; -import { useChatCommandSelector, useChatInputWidget, useRoom, useSessionInfo, useUiEvent } from '../../../../hooks'; +import { useChatCommandSelector, useChatInputWidget, useMentionAutocomplete, useRoom, useSessionInfo, useUiEvent } from '../../../../hooks'; import { ChatInputCommandSelectorView } from './ChatInputCommandSelectorView'; import { ChatInputEmojiSelectorView } from './ChatInputEmojiSelectorView'; +import { ChatInputMentionSelectorView } from './ChatInputMentionSelectorView'; import { ChatInputStyleSelectorView } from './ChatInputStyleSelectorView'; export const ChatInputView: FC<{}> = props => @@ -16,6 +17,7 @@ export const ChatInputView: FC<{}> = props => const { roomSession = null } = useRoom(); const inputRef = useRef(null); const { isVisible: commandSelectorVisible, filteredCommands, selectedIndex, setSelectedIndex, moveUp, moveDown, selectCurrent, close: closeCommandSelector } = useChatCommandSelector(chatValue); + const mention = useMentionAutocomplete(chatValue); const chatModeIdWhisper = useMemo(() => LocalizeText('widgets.chatinput.mode.whisper'), []); const chatModeIdShout = useMemo(() => LocalizeText('widgets.chatinput.mode.shout'), []); @@ -171,6 +173,36 @@ export const ChatInputView: FC<{}> = props => const value = (event.target as HTMLInputElement).value; + if(mention.isVisible) + { + switch(event.key) + { + case 'ArrowUp': + event.preventDefault(); + mention.moveUp(); + return; + case 'ArrowDown': + event.preventDefault(); + mention.moveDown(); + return; + case 'Tab': + event.preventDefault(); + // fall through + case 'NumpadEnter': + case 'Enter': { + const current = mention.current(); + + if(current) + { + event.preventDefault(); + setChatValue(prev => mention.applyTo(prev, current.name)); + return; + } + break; + } + } + } + switch(event.key) { case ' ': @@ -194,7 +226,7 @@ export const ChatInputView: FC<{}> = props => return; } - }, [ floodBlocked, inputRef, chatModeIdWhisper, anotherInputHasFocus, setInputFocus, checkSpecialKeywordForInput, sendChatValue, commandSelectorVisible, moveUp, moveDown, selectCurrent, closeCommandSelector ]); + }, [ floodBlocked, inputRef, chatModeIdWhisper, anotherInputHasFocus, setInputFocus, checkSpecialKeywordForInput, sendChatValue, commandSelectorVisible, moveUp, moveDown, selectCurrent, closeCommandSelector, mention ]); useUiEvent(RoomWidgetUpdateChatInputContentEvent.CHAT_INPUT_CONTENT, event => { @@ -290,6 +322,16 @@ export const ChatInputView: FC<{}> = props => } } onHover={ setSelectedIndex } /> } + { (!commandSelectorVisible && mention.isVisible) && + + { + setChatValue(prev => mention.applyTo(prev, suggestion.name)); inputRef.current?.focus(); + } } + onHover={ mention.setSelectedIndex } + /> }
{ !floodBlocked && updateChatInput(event.target.value) } onMouseDown={ event => setInputFocus() } /> } diff --git a/src/components/room/widgets/chat/highlightMentions.tsx b/src/components/room/widgets/chat/highlightMentions.tsx index 15d32dd..14f0cc0 100644 --- a/src/components/room/widgets/chat/highlightMentions.tsx +++ b/src/components/room/widgets/chat/highlightMentions.tsx @@ -51,9 +51,14 @@ const isMentionToken = (token: string, ownUsernameLower: string, aliases: Readon if(!nick) return false; + // Own nick and room-broadcast aliases always count. if(ownUsernameLower && nick === ownUsernameLower) return true; + if(aliases.has(nick)) return true; - return aliases.has(nick); + // Any other valid @nick token is also highlighted (blue), so a direct + // @username mention reads the same as @all — visual feedback that it is a + // recognised mention. (Cosmetic only; the server decides actual delivery.) + return true; }; /** diff --git a/src/css/mentions/MentionToasts.css b/src/css/mentions/MentionToasts.css new file mode 100644 index 0000000..6895c89 --- /dev/null +++ b/src/css/mentions/MentionToasts.css @@ -0,0 +1,131 @@ +.mention-toasts { + position: fixed; + top: 130px; + right: 12px; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 8px; + max-width: 320px; + pointer-events: none; +} + +.mention-toast { + pointer-events: auto; + display: flex; + align-items: center; + gap: 8px; + width: 300px; + padding: 8px 10px; + background: #2b2f3a; + color: #fff; + border: 1px solid rgba(255, 255, 255, 0.12); + border-left: 3px solid #1e7295; + border-radius: 8px; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.42); + cursor: pointer; + animation: mention-toast-in 0.22s ease; +} + +@keyframes mention-toast-in { + from { opacity: 0; transform: translateX(22px); } + to { opacity: 1; transform: translateX(0); } +} + +.mention-toast-avatar { + position: relative; + width: 44px; + height: 44px; + flex-shrink: 0; + overflow: hidden; + border-radius: 6px; + background: rgba(0, 0, 0, 0.25); +} + +/* ricetta testa headOnly: l'avatar-image riempie il box (inset-0) e si croppa + sulla testa via background-position (come l'avatar-testa della toolbar), + invece di scalare il corpo. */ +.mention-toast-avatar .avatar-image { + position: absolute !important; + inset: 0 !important; + width: 100% !important; + height: 100% !important; + background-size: auto !important; + background-position: -23px -32px !important; +} + +.mention-toast-body { + flex: 1 1 auto; + min-width: 0; +} + +.mention-toast-title { + font-weight: 700; + font-size: 12px; + line-height: 1.2; + color: #6cb6e0; +} + +.mention-toast-message { + font-size: 12px; + line-height: 1.3; + color: #e6e8ec; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + word-break: break-word; +} + +.mention-toast-dismiss { + flex-shrink: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border: none; + border-radius: 50%; + background: rgba(255, 255, 255, 0.08); + color: #c9ccd3; + cursor: pointer; + font-size: 10px; + line-height: 1; +} + +.mention-toast-dismiss:hover { + background: rgba(255, 255, 255, 0.18); + color: #fff; +} + +/* dropdown autocomplete @ : testine + alias */ +.mention-suggest-avatar { + position: relative; + width: 32px; + height: 32px; + flex-shrink: 0; + overflow: hidden; + border-radius: 4px; +} + +.mention-suggest-avatar .avatar-image { + position: absolute !important; + inset: 0 !important; + width: 100% !important; + height: 100% !important; + background-size: auto !important; + background-position: -27px -34px !important; +} + +.mention-suggest-alias { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + font-weight: 700; + color: #1e7295; + background: rgba(30, 114, 149, 0.12); + border-radius: 4px; +} diff --git a/src/hooks/catalog/useCatalog.ts b/src/hooks/catalog/useCatalog.ts index 807b98e..807ef66 100644 --- a/src/hooks/catalog/useCatalog.ts +++ b/src/hooks/catalog/useCatalog.ts @@ -1,4 +1,4 @@ -import { BuildersClubFurniCountMessageEvent, BuildersClubPlaceRoomItemMessageComposer, BuildersClubPlaceWallItemMessageComposer, BuildersClubQueryFurniCountMessageComposer, BuildersClubSubscriptionStatusMessageEvent, CatalogPageMessageEvent, CatalogPagesListEvent, CatalogPublishedMessageEvent, CreateLinkEvent, FrontPageItem, FurniturePlaceComposer, FurniturePlacePaintComposer, GetCatalogIndexComposer, GetCatalogPageComposer, GetRoomEngine, GetSessionDataManager, GetTickerTime, LegacyDataType, LimitedEditionSoldOutEvent, MarketplaceMakeOfferResult, ProductOfferEvent, PurchaseErrorMessageEvent, PurchaseFromCatalogComposer, PurchaseNotAllowedMessageEvent, PurchaseOKMessageEvent, RoomEngineObjectPlacedEvent, RoomObjectPlacementSource, RoomObjectVariable, RoomPreviewer, Vector3d } from '@nitrots/nitro-renderer'; +import { BuildersClubFurniCountMessageEvent, BuildersClubPlaceRoomItemMessageComposer, BuildersClubPlaceWallItemMessageComposer, BuildersClubQueryFurniCountMessageComposer, BuildersClubSubscriptionStatusMessageEvent, CatalogPageMessageEvent, CatalogPagesListEvent, CatalogPublishedMessageEvent, CreateLinkEvent, FrontPageItem, FurniturePlaceComposer, FurniturePlacePaintComposer, GetCatalogIndexComposer, GetCatalogPageComposer, GetConfiguration, GetRoomContentLoader, GetRoomEngine, GetSessionDataManager, GetTickerTime, LegacyDataType, LimitedEditionSoldOutEvent, MarketplaceMakeOfferResult, ProductOfferEvent, PurchaseErrorMessageEvent, PurchaseFromCatalogComposer, PurchaseNotAllowedMessageEvent, PurchaseOKMessageEvent, RoomEngineObjectPlacedEvent, RoomObjectPlacementSource, RoomObjectVariable, RoomPreviewer, Vector3d } from '@nitrots/nitro-renderer'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useBetween } from 'use-between'; import { BuilderFurniPlaceableStatus, CatalogPage, CatalogType, DispatchUiEvent, FurniCategory, GetFurnitureData, GetProductDataForLocalization, GetRoomSession, ICatalogNode, ICatalogPage, IPageLocalization, IProduct, IPurchasableOffer, IPurchaseOptions, LocalizeText, NotificationAlertType, Offer, PageLocalization, PlacedObjectPurchaseData, PlaySound, Product, ProductTypeEnum, RequestedPage, SearchResult, SendMessageComposer, SoundNames } from '../../api'; @@ -89,6 +89,27 @@ const useCatalogStore = () => setCurrentType(normalizeCatalogType(type)); }, []); + // Real-time furni importati: ri-mergia il chunk custom/imported.json5 nelle Map + // furnidata + RoomContentLoader all'apertura del catalogo, SENZA reload del client. + const refreshImportedFurnidata = useCallback(() => + { + try + { + const base = GetConfiguration().getValue('furnidata.url'); + + if(!base || !base.length) return; + + const importedUrl = base.replace(/\/+$/, '') + '/custom/imported.json5'; + + GetSessionDataManager().mergeFurnitureDataFromUrl(importedUrl).then(added => + { + if(added && added.length) GetRoomContentLoader().processFurnitureData(added); + }).catch(() => {}); + } + catch + {} + }, []); + const openCatalogByType = useCallback((type?: string) => { const catalogType = normalizeCatalogType(type); @@ -98,8 +119,10 @@ const useCatalogStore = () => resetVisibleCatalogState(catalogType); } + refreshImportedFurnidata(); + setIsVisible(true); - }, [ currentType, resetVisibleCatalogState ]); + }, [ currentType, resetVisibleCatalogState, refreshImportedFurnidata ]); const toggleCatalogByType = useCallback((type?: string) => { @@ -117,8 +140,10 @@ const useCatalogStore = () => resetVisibleCatalogState(catalogType); } + refreshImportedFurnidata(); + setIsVisible(true); - }, [ isVisible, currentType, resetVisibleCatalogState ]); + }, [ isVisible, currentType, resetVisibleCatalogState, refreshImportedFurnidata ]); const getBuilderFurniPlaceableStatus = useCallback((offer: IPurchasableOffer) => { diff --git a/src/hooks/mentions/__tests__/mentionsStore.test.ts b/src/hooks/mentions/__tests__/mentionsStore.test.ts index cc3557d..e85b5fb 100644 --- a/src/hooks/mentions/__tests__/mentionsStore.test.ts +++ b/src/hooks/mentions/__tests__/mentionsStore.test.ts @@ -3,7 +3,7 @@ import { addMention, setMentions, markAllRead, markRead, getMentionsSnapshot, ge import { IMentionEntry } from '../../../api/mentions'; const make = (id: number, read = false): IMentionEntry => ({ - mentionId: id, senderId: 1, senderUsername: 'Bob', roomId: 9, roomName: 'R', + mentionId: id, senderId: 1, senderUsername: 'Bob', senderFigure: '', roomId: 9, roomName: 'R', message: '@me hi', mentionType: 0, timestamp: 0, read }); diff --git a/src/hooks/mentions/index.ts b/src/hooks/mentions/index.ts index 3486da8..f8b9e5f 100644 --- a/src/hooks/mentions/index.ts +++ b/src/hooks/mentions/index.ts @@ -1,2 +1,3 @@ export * from './useMentionsSnapshot'; export * from './useMentionMessages'; +export * from './useMentionAutocomplete'; diff --git a/src/hooks/mentions/mentionToastsStore.ts b/src/hooks/mentions/mentionToastsStore.ts new file mode 100644 index 0000000..07c5fec --- /dev/null +++ b/src/hooks/mentions/mentionToastsStore.ts @@ -0,0 +1,58 @@ +import { IMentionEntry } from '../../api'; + +// Toast laterali per le menzioni appena ricevute (avatar + messaggio + dismiss). +// Separato da mentionsStore: i toast sono effimeri, le menzioni persistono nel pannello. +export interface MentionToast +{ + mentionId: number; + senderId: number; + senderUsername: string; + senderFigure: string; + message: string; + roomName: string; +} + +const MAX_TOASTS = 4; + +let toasts: MentionToast[] = []; +const listeners = new Set<() => void>(); + +const emit = (): void => +{ + for(const listener of listeners) listener(); +}; + +export const subscribeMentionToasts = (callback: () => void): (() => void) => +{ + listeners.add(callback); + return () => { listeners.delete(callback); }; +}; + +export const getMentionToasts = (): ReadonlyArray => toasts; + +export const pushMentionToast = (entry: IMentionEntry): void => +{ + toasts = [ + { + mentionId: entry.mentionId, + senderId: entry.senderId, + senderUsername: entry.senderUsername, + senderFigure: entry.senderFigure, + message: entry.message, + roomName: entry.roomName + }, + ...toasts.filter(toast => toast.mentionId !== entry.mentionId) + ].slice(0, MAX_TOASTS); + + emit(); +}; + +export const dismissMentionToast = (mentionId: number): void => +{ + const next = toasts.filter(toast => toast.mentionId !== mentionId); + + if(next.length === toasts.length) return; + + toasts = next; + emit(); +}; diff --git a/src/hooks/mentions/useMentionAutocomplete.ts b/src/hooks/mentions/useMentionAutocomplete.ts new file mode 100644 index 0000000..6b2bbc1 --- /dev/null +++ b/src/hooks/mentions/useMentionAutocomplete.ts @@ -0,0 +1,89 @@ +import { useEffect, useMemo, useState } from 'react'; +import { MENTION_ROOM_ALIASES } from '../../components/room/widgets/chat/highlightMentions'; +import { useFriendsState } from '../friends/useFriends'; +import { useRoomUserListSnapshot } from '../session/useSessionSnapshots'; + +export interface MentionSuggestion +{ + name: string; + figure: string; + isAlias: boolean; +} + +const MAX_SUGGESTIONS = 8; + +// Trova il token @ che si sta digitando alla FINE del valore. +// Restituisce il parziale (anche '' subito dopo @) oppure null se non si è in un @mention. +const activeMentionPartial = (value: string): string | null => +{ + if(!value || value.indexOf('@') < 0) return null; + + const match = /(?:^|\s)@([A-Za-z0-9_]*)$/.exec(value); + + return match ? match[1] : null; +}; + +export interface MentionAutocompleteState +{ + isVisible: boolean; + suggestions: MentionSuggestion[]; + selectedIndex: number; + setSelectedIndex: (index: number) => void; + moveUp: () => void; + moveDown: () => void; + current: () => MentionSuggestion | null; + // Inserisce il nome scelto sostituendo il parziale @... alla fine del valore. + applyTo: (value: string, name: string) => string; +} + +export const useMentionAutocomplete = (chatValue: string): MentionAutocompleteState => +{ + const roomUsers = useRoomUserListSnapshot(); + const { onlineFriends } = useFriendsState(); + const [ selectedIndex, setSelectedIndex ] = useState(0); + + const partial = useMemo(() => activeMentionPartial(chatValue), [ chatValue ]); + + const suggestions = useMemo(() => + { + if(partial === null) return []; + + const query = partial.toLowerCase(); + const seen = new Set(); + const out: MentionSuggestion[] = []; + + const add = (name: string, figure: string, isAlias: boolean) => + { + if(!name || out.length >= MAX_SUGGESTIONS) return; + + const key = name.toLowerCase(); + + if(seen.has(key)) return; + if(query && !key.startsWith(query)) return; + + seen.add(key); + out.push({ name, figure: figure || '', isAlias }); + }; + + for(const user of (roomUsers || [])) add(user?.name, (user as any)?.figure, false); + for(const friend of (onlineFriends || [])) add(friend?.name, friend?.figure, false); + for(const alias of MENTION_ROOM_ALIASES) add(alias, '', true); + + return out; + }, [ partial, roomUsers, onlineFriends ]); + + useEffect(() => { setSelectedIndex(0); }, [ partial ]); + + const isVisible = (partial !== null) && (suggestions.length > 0); + + return { + isVisible, + suggestions, + selectedIndex, + setSelectedIndex, + moveUp: () => setSelectedIndex(index => (index <= 0 ? suggestions.length - 1 : index - 1)), + moveDown: () => setSelectedIndex(index => (index >= suggestions.length - 1 ? 0 : index + 1)), + current: () => suggestions[selectedIndex] ?? null, + applyTo: (value: string, name: string) => value.replace(/@([A-Za-z0-9_]*)$/, '@' + name + ' ') + }; +}; diff --git a/src/hooks/mentions/useMentionMessages.ts b/src/hooks/mentions/useMentionMessages.ts index 192b828..09cf0dd 100644 --- a/src/hooks/mentions/useMentionMessages.ts +++ b/src/hooks/mentions/useMentionMessages.ts @@ -1,17 +1,15 @@ import { MentionReceivedEvent, MentionsListEvent, RequestMentionsComposer } from '@nitrots/nitro-renderer'; import { useCallback, useEffect } from 'react'; -import { GetConfigurationValue, IMentionEntry, LocalizeText, NotificationBubbleType, PlaySound, SendMessageComposer } from '../../api'; +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/.mp3. const MENTION_SOUND_SAMPLE = 'mentions_notification'; export const useMentionMessages = (): void => { - const { showSingleBubble } = useNotificationActions(); - const onMentionsList = useCallback((event: MentionsListEvent) => { const list = event.getParser().mentions; @@ -20,6 +18,7 @@ export const useMentionMessages = (): void => mentionId: m.mentionId, senderId: m.senderId, senderUsername: m.senderUsername, + senderFigure: m.senderFigure, roomId: m.roomId, roomName: m.roomName, message: m.message, @@ -39,6 +38,7 @@ export const useMentionMessages = (): void => mentionId: m.mentionId, senderId: m.senderId, senderUsername: m.senderUsername, + senderFigure: m.senderFigure, roomId: m.roomId, roomName: m.roomName, message: m.message, @@ -51,14 +51,9 @@ export const useMentionMessages = (): void => if(GetConfigurationValue('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, onMentionsList); useMessageEvent(MentionReceivedEvent, onMentionReceived); diff --git a/src/index.tsx b/src/index.tsx index 3825168..d1f1b0f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -23,6 +23,7 @@ import './css/catalog/CatalogClassicView.css'; import './css/emustats/EmuStatsView.css'; import './css/chat/Chats.css'; +import './css/mentions/MentionToasts.css'; import './css/common/Buttons.css';