mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
feat(mentions): inbox window, toolbar badge, chat-history tab, ui-config + i18n
This commit is contained in:
@@ -772,4 +772,13 @@
|
||||
'usersettings.success.password': "Password aggiornata con successo.",
|
||||
'usersettings.success.email': "Email aggiornata con successo.",
|
||||
'usersettings.success.username': "Nome utente aggiornato. Accedi di nuovo con il tuo nuovo nome.",
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Mentions
|
||||
// ------------------------------------------------------------------------
|
||||
'mentions.window.title': 'Menzioni',
|
||||
'mentions.window.empty': 'Nessuna menzione',
|
||||
'mentions.window.markall': 'Segna tutte come lette',
|
||||
'mentions.tab.title': 'Menzioni',
|
||||
'mentions.notification': '%sender% ti ha menzionato in %room%',
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
"wired.action.mute.user.max.length": 100,
|
||||
"game.center.enabled": false,
|
||||
"radio_ui.enabled": false,
|
||||
"mentions_ui.enabled": true,
|
||||
"mentions_ui.sound": true,
|
||||
"guides.enabled": true,
|
||||
"housekeeping.enabled": true,
|
||||
"toolbar.hide.quests": true,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { AddLinkEventTracker, GetCommunication, GetRoomSessionManager, HabboWebTools, ILinkEventTracker, RemoveLinkEventTracker, RoomSessionEvent } from '@nitrots/nitro-renderer';
|
||||
import { AddLinkEventTracker, GetCommunication, GetRoomSessionManager, HabboWebTools, ILinkEventTracker, MarkMentionsReadComposer, RemoveLinkEventTracker, RoomSessionEvent } from '@nitrots/nitro-renderer';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { GetConfigurationValue } from '../api';
|
||||
import { useNitroEventReducer } from '../hooks';
|
||||
import { GetConfigurationValue, SendMessageComposer } from '../api';
|
||||
import { useMentionMessages, useNitroEventReducer } from '../hooks';
|
||||
import { markAllRead } from '../hooks/mentions/mentionsStore';
|
||||
import { AchievementsView } from './achievements/AchievementsView';
|
||||
import { AvatarEditorView } from './avatar-editor';
|
||||
import { BadgeCreatorView } from './badge-creator';
|
||||
@@ -47,11 +48,15 @@ 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';
|
||||
|
||||
export const MainView: FC<{}> = props =>
|
||||
{
|
||||
const [ isReady, setIsReady ] = useState(false);
|
||||
const [ localizationVersion, setLocalizationVersion ] = useState(0);
|
||||
const [ mentionsVisible, setMentionsVisible ] = useState(false);
|
||||
|
||||
useMentionMessages();
|
||||
|
||||
// CREATED and ENDED can arrive out of order under flaky reconnects.
|
||||
// Treating them as two independent setters left landingViewVisible
|
||||
@@ -124,6 +129,54 @@ export const MainView: FC<{}> = props =>
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
// Opening the inbox clears the unread badge both locally and
|
||||
// server-side so the toolbar count resets immediately.
|
||||
const clearMentionsBadge = () =>
|
||||
{
|
||||
markAllRead();
|
||||
SendMessageComposer(new MarkMentionsReadComposer(0, 0));
|
||||
};
|
||||
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived: (url: string) =>
|
||||
{
|
||||
const parts = url.split('/');
|
||||
|
||||
if(parts.length < 2) return;
|
||||
|
||||
switch(parts[1])
|
||||
{
|
||||
case 'show':
|
||||
setMentionsVisible(true);
|
||||
clearMentionsBadge();
|
||||
return;
|
||||
case 'hide':
|
||||
setMentionsVisible(false);
|
||||
return;
|
||||
case 'toggle':
|
||||
setMentionsVisible(prevValue =>
|
||||
{
|
||||
if(prevValue) return false;
|
||||
|
||||
// Side-effect-free in the updater: defer the
|
||||
// badge-clear to a microtask so React's
|
||||
// double-invoke (StrictMode) can't fire it twice.
|
||||
queueMicrotask(clearMentionsBadge);
|
||||
return true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
},
|
||||
eventUrlPrefix: 'mentions/'
|
||||
};
|
||||
|
||||
AddLinkEventTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const refreshLocalization = () => setLocalizationVersion(value => (value + 1));
|
||||
@@ -187,6 +240,8 @@ export const MainView: FC<{}> = props =>
|
||||
<FortuneWheelView />
|
||||
<SoundboardView />
|
||||
{ GetConfigurationValue<boolean>('radio_ui.enabled', false) && <RadioView /> }
|
||||
{ (GetConfigurationValue<boolean>('mentions_ui.enabled', true) && mentionsVisible) &&
|
||||
<MentionsView onClose={ () => setMentionsVisible(false) } /> }
|
||||
<ExternalPluginLoader />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ChatEntryType, LocalizeText } from '../../api';
|
||||
import { Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common';
|
||||
import { useChatHistory, useOnClickChat } from '../../hooks';
|
||||
import { Flex, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Text } from '../../common';
|
||||
import { useChatHistory, useMentionsSnapshot, useOnClickChat } from '../../hooks';
|
||||
import { NitroInput } from '../../layout';
|
||||
import { MentionRowView, useMentionRowClick } from '../mentions';
|
||||
|
||||
const TAB_CHAT = 'chat';
|
||||
const TAB_MENTIONS = 'mentions';
|
||||
|
||||
export const ChatHistoryView: FC<{}> = props =>
|
||||
{
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [activeTab, setActiveTab] = useState<string>(TAB_CHAT);
|
||||
const { chatHistory = [] } = useChatHistory();
|
||||
const { mentions } = useMentionsSnapshot();
|
||||
const onMentionRowClick = useMentionRowClick();
|
||||
const { onClickChat } = useOnClickChat();
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const isFirstRender = useRef(true);
|
||||
@@ -87,7 +94,25 @@ export const ChatHistoryView: FC<{}> = props =>
|
||||
return (
|
||||
<NitroCardView className="w-[400px] h-[400px] bg-[#f0f0f0]" theme="primary-slim" uniqueKey="chat-history">
|
||||
<NitroCardHeaderView headerText={LocalizeText('room.chathistory.button.text')} onCloseClick={event => setIsVisible(false)} />
|
||||
<NitroCardTabsView>
|
||||
<NitroCardTabsItemView isActive={ activeTab === TAB_CHAT } onClick={ () => setActiveTab(TAB_CHAT) }>
|
||||
{ LocalizeText('room.chathistory.button.text') }
|
||||
</NitroCardTabsItemView>
|
||||
<NitroCardTabsItemView count={ mentions.reduce((n, m) => n + (m.read ? 0 : 1), 0) } isActive={ activeTab === TAB_MENTIONS } onClick={ () => setActiveTab(TAB_MENTIONS) }>
|
||||
{ LocalizeText('mentions.tab.title') }
|
||||
</NitroCardTabsItemView>
|
||||
</NitroCardTabsView>
|
||||
<NitroCardContentView className="h-full bg-[#f0f0f0] bg-[url('@/assets/images/chat/chathistory_background.png')] bg-repeat bg-auto" gap={2} overflow="hidden" style={{ height: 'calc(100% - 40px)', display: 'flex', flexDirection: 'column' }}>
|
||||
{ activeTab === TAB_MENTIONS ? (
|
||||
<div style={{ flex: 1, overflowY: 'auto', background: 'inherit' }}>
|
||||
{ (mentions.length === 0)
|
||||
? <Text center variant="gray">{ LocalizeText('mentions.window.empty') }</Text>
|
||||
: mentions.map(mention => (
|
||||
<MentionRowView key={ mention.mentionId } mention={ mention } onClick={ onMentionRowClick } />
|
||||
)) }
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<NitroInput placeholder={LocalizeText('generic.search')} type="text" value={searchText} onChange={event => setSearchText(event.target.value)} />
|
||||
<div ref={elementRef} style={{ flex: 1, overflowY: 'auto', background: 'inherit' }}>
|
||||
{filteredChatHistory.map((row, index) => (
|
||||
@@ -119,6 +144,8 @@ export const ChatHistoryView: FC<{}> = props =>
|
||||
</Flex>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) }
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { FC } from 'react';
|
||||
import { IMentionEntry } from '../../api';
|
||||
import { Flex, Text } from '../../common';
|
||||
|
||||
interface MentionRowViewProps
|
||||
{
|
||||
mention: IMentionEntry;
|
||||
onClick: (mention: IMentionEntry) => void;
|
||||
}
|
||||
|
||||
export const MentionRowView: FC<MentionRowViewProps> = props =>
|
||||
{
|
||||
const { mention, onClick } = props;
|
||||
|
||||
return (
|
||||
<Flex pointer alignItems="center" className="p-1 hover:bg-black/5" gap={ 2 } onClick={ () => onClick(mention) }>
|
||||
<span
|
||||
className={ `inline-block w-[8px] h-[8px] rounded-full shrink-0 ${ mention.read ? 'bg-transparent' : 'bg-[#1e7295]' }` } />
|
||||
<Flex grow column className="min-w-0" gap={ 0 }>
|
||||
<Flex alignItems="center" gap={ 1 }>
|
||||
<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>
|
||||
<Text truncate variant="black">{ mention.message }</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { MarkMentionsReadComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback } from 'react';
|
||||
import { LocalizeText, SendMessageComposer } from '../../api';
|
||||
import { Button, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common';
|
||||
import { useMentionsSnapshot } from '../../hooks';
|
||||
import { markAllRead } from '../../hooks/mentions/mentionsStore';
|
||||
import { MentionRowView } from './MentionRowView';
|
||||
import { useMentionRowClick } from './useMentionRowClick';
|
||||
|
||||
interface MentionsViewProps
|
||||
{
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const MentionsView: FC<MentionsViewProps> = props =>
|
||||
{
|
||||
const { onClose } = props;
|
||||
const { mentions } = useMentionsSnapshot();
|
||||
const onRowClick = useMentionRowClick();
|
||||
|
||||
const onMarkAll = useCallback(() =>
|
||||
{
|
||||
markAllRead();
|
||||
SendMessageComposer(new MarkMentionsReadComposer(0, 0));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<NitroCardView className="w-[340px] h-[420px]" theme="primary-slim" uniqueKey="mentions">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('mentions.window.title') } onCloseClick={ onClose } />
|
||||
<NitroCardContentView gap={ 1 }>
|
||||
<Flex grow column className="min-h-0 overflow-y-auto" gap={ 0 }>
|
||||
{ (mentions.length === 0)
|
||||
? <Text center variant="gray">{ LocalizeText('mentions.window.empty') }</Text>
|
||||
: mentions.map(mention => (
|
||||
<MentionRowView key={ mention.mentionId } mention={ mention } onClick={ onRowClick } />
|
||||
)) }
|
||||
</Flex>
|
||||
{ (mentions.length > 0) &&
|
||||
<Button variant="primary" onClick={ onMarkAll }>{ LocalizeText('mentions.window.markall') }</Button> }
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './MentionRowView';
|
||||
export * from './MentionsView';
|
||||
export * from './useMentionRowClick';
|
||||
@@ -0,0 +1,20 @@
|
||||
import { CreateLinkEvent, MarkMentionsReadComposer } from '@nitrots/nitro-renderer';
|
||||
import { useCallback } from 'react';
|
||||
import { IMentionEntry, SendMessageComposer } from '../../api';
|
||||
import { markRead } from '../../hooks/mentions/mentionsStore';
|
||||
|
||||
// Shared row-click handler used by both MentionsView and the chat-history
|
||||
// "Menzioni" tab so the mark-read + room-navigation behaviour can't diverge.
|
||||
export const useMentionRowClick = (): ((mention: IMentionEntry) => void) =>
|
||||
{
|
||||
return useCallback((mention: IMentionEntry) =>
|
||||
{
|
||||
if(!mention.read)
|
||||
{
|
||||
markRead(mention.mentionId);
|
||||
SendMessageComposer(new MarkMentionsReadComposer(1, mention.mentionId));
|
||||
}
|
||||
|
||||
if(mention.roomId > 0) CreateLinkEvent(`navigator/goto/${ mention.roomId }`);
|
||||
}, []);
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { AnimatePresence, motion, Variants } from 'framer-motion';
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { GetConfigurationValue, isHousekeepingEnabled, MessengerIconState, OpenMessengerChat, setYoutubeRoomEnabled, VisitDesktop } from '../../api';
|
||||
import { Flex, LayoutAvatarImageView, LayoutItemCountView } from '../../common';
|
||||
import { useAchievements, useFriends, useHasPermission, useInventoryUnseenTracker, useMessageEvent, useMessenger, useModTools, useNitroEvent, useSessionInfo, useSoundboard, useWiredTools } from '../../hooks';
|
||||
import { useAchievements, useFriends, useHasPermission, useInventoryUnseenTracker, useMentionsSnapshot, useMessageEvent, useMessenger, useModTools, useNitroEvent, useSessionInfo, useSoundboard, useWiredTools } from '../../hooks';
|
||||
import { ToolbarItemView } from './ToolbarItemView';
|
||||
import { ToolbarMeView } from './ToolbarMeView';
|
||||
import { YouTubePlayerView } from './YouTubePlayerView';
|
||||
@@ -42,6 +42,8 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
const { getTotalUnseen = 0 } = useAchievements();
|
||||
const { requests = [] } = useFriends();
|
||||
const { iconState = MessengerIconState.HIDDEN } = useMessenger();
|
||||
const { unreadCount: mentionsUnread = 0 } = useMentionsSnapshot();
|
||||
const mentionsEnabled = useMemo(() => GetConfigurationValue<boolean>('mentions_ui.enabled', true), []);
|
||||
const { openMonitor, showToolbarButton } = useWiredTools();
|
||||
const { enabled: soundboardEnabled, reset: resetSoundboard } = useSoundboard();
|
||||
const isMod = useHasPermission('acc_supporttool');
|
||||
@@ -332,6 +334,12 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
{ (requests.length > 0) &&
|
||||
<LayoutItemCountView count={ requests.length } className="absolute -right-2 -top-1" /> }
|
||||
</motion.div>
|
||||
{ mentionsEnabled &&
|
||||
<motion.div variants={ itemVariants } className="relative">
|
||||
<ToolbarItemView icon="message" onClick={ () => CreateLinkEvent('mentions/toggle') } className="tb-icon" />
|
||||
{ (mentionsUnread > 0) &&
|
||||
<LayoutItemCountView count={ mentionsUnread } className="absolute -right-2 -top-1" /> }
|
||||
</motion.div> }
|
||||
{ ((iconState === MessengerIconState.SHOW) || (iconState === MessengerIconState.UNREAD)) &&
|
||||
<motion.div variants={ itemVariants }>
|
||||
<ToolbarItemView className={ `tb-icon ${ iconState === MessengerIconState.UNREAD ? 'is-unseen animate-pulse' : '' }` } icon="message" onClick={ () => OpenMessengerChat() } />
|
||||
@@ -422,6 +430,12 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
{ (requests.length > 0) &&
|
||||
<LayoutItemCountView count={ requests.length } className="absolute -right-2 -top-1" /> }
|
||||
</motion.div>
|
||||
{ mentionsEnabled &&
|
||||
<motion.div variants={ itemVariants } className="relative">
|
||||
<ToolbarItemView icon="message" onClick={ () => CreateLinkEvent('mentions/toggle') } className="tb-icon" />
|
||||
{ (mentionsUnread > 0) &&
|
||||
<LayoutItemCountView count={ mentionsUnread } className="absolute -right-2 -top-1" /> }
|
||||
</motion.div> }
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
{ /* Mobile side tools — moved out of the bottom bar into a
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { MarkMentionsReadComposer, MentionReceivedEvent, MentionsListEvent, RequestMentionsComposer } from '@nitrots/nitro-renderer';
|
||||
import { MentionReceivedEvent, MentionsListEvent, RequestMentionsComposer } from '@nitrots/nitro-renderer';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { GetConfigurationValue, IMentionEntry, LocalizeText, NotificationBubbleType, PlaySound, SendMessageComposer, SoundNames } from '../../api';
|
||||
import { useMessageEvent } from '../events';
|
||||
import { useNotificationActions } from '../notification';
|
||||
import { addMention, setMentions } from './mentionsStore';
|
||||
|
||||
// MarkMentionsReadComposer is part of the mentions wire contract; it is sent by
|
||||
// the UI layer (later phase) when the user opens / clears the mentions window.
|
||||
void MarkMentionsReadComposer;
|
||||
|
||||
export const useMentionMessages = (): void =>
|
||||
{
|
||||
const { showSingleBubble } = useNotificationActions();
|
||||
|
||||
@@ -251,6 +251,11 @@ export class FlatAccessDeniedMessageEvent extends MessageEvent {}
|
||||
export class GenericErrorEvent extends MessageEvent {}
|
||||
export class GetGuestRoomResultEvent extends MessageEvent {}
|
||||
|
||||
// Mentions system — incoming events extend MessageEvent (they expose
|
||||
// getParser()); the request/mark composers are symbol-only constructors.
|
||||
export class MentionReceivedEvent extends MessageEvent {}
|
||||
export class MentionsListEvent extends MessageEvent {}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Navigator event classes — MessageEvent subclasses needed by useNavigatorStore
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -377,6 +382,8 @@ export class FollowFriendMessageComposer extends StubClass {}
|
||||
export class GetUserEventCatsMessageComposer extends StubClass {}
|
||||
export class GetUserFlatCatsMessageComposer extends StubClass {}
|
||||
export class NavigatorSearchComposer extends StubClass {}
|
||||
export class RequestMentionsComposer extends StubClass {}
|
||||
export class MarkMentionsReadComposer extends StubClass {}
|
||||
export class DesktopViewComposer extends StubClass {}
|
||||
export class FurniturePlacePaintComposer extends StubClass {}
|
||||
export class GetGuestRoomMessageComposer extends StubClass {}
|
||||
|
||||
Reference in New Issue
Block a user