mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
Merge remote-tracking branch 'upstream/main'
# Conflicts: # public/UITexts.example # src/api/wired/WiredActionLayoutCode.ts # src/api/wired/WiredConditionLayoutCode.ts # src/api/wired/WiredTriggerLayoutCode.ts # src/components/wired/views/WiredBaseView.tsx # src/components/wired/views/WiredSourcesSelector.tsx # src/components/wired/views/actions/WiredActionLayoutView.tsx # src/components/wired/views/conditions/WiredConditionLayoutView.tsx # src/components/wired/views/conditions/WiredConditionTriggererMatchView.tsx # src/components/wired/views/triggers/WiredTriggerClickFurniView.tsx # src/components/wired/views/triggers/WiredTriggerClickTileView.tsx # src/components/wired/views/triggers/WiredTriggerClickUserView.tsx # src/components/wired/views/triggers/WiredTriggerLayoutView.tsx # src/components/wired/views/triggers/WiredTriggerToggleFurniView.tsx
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export * from './useFurniEditor';
|
||||
@@ -0,0 +1,239 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
export interface FurniItem
|
||||
{
|
||||
id: number;
|
||||
spriteId: number;
|
||||
itemName: string;
|
||||
publicName: string;
|
||||
type: string;
|
||||
width: number;
|
||||
length: number;
|
||||
stackHeight: number;
|
||||
allowStack: boolean;
|
||||
allowWalk: boolean;
|
||||
allowSit: boolean;
|
||||
allowLay: boolean;
|
||||
interactionType: string;
|
||||
interactionModesCount: number;
|
||||
}
|
||||
|
||||
export interface FurniDetail extends FurniItem
|
||||
{
|
||||
allowGift: boolean;
|
||||
allowTrade: boolean;
|
||||
allowRecycle: boolean;
|
||||
allowMarketplaceSell: boolean;
|
||||
allowInventoryStack: boolean;
|
||||
vendingIds: string;
|
||||
customparams: string;
|
||||
effectIdMale: number;
|
||||
effectIdFemale: number;
|
||||
clothingOnWalk: string;
|
||||
multiheight: string;
|
||||
description: string;
|
||||
usageCount: number;
|
||||
}
|
||||
|
||||
export interface CatalogRef
|
||||
{
|
||||
id: number;
|
||||
catalogName: string;
|
||||
costCredits: number;
|
||||
costPoints: number;
|
||||
pointsType: number;
|
||||
pageId: number;
|
||||
pageName: string;
|
||||
}
|
||||
|
||||
const API_BASE = '/api/admin/furni-editor';
|
||||
|
||||
async function apiFetch<T>(url: string, options?: RequestInit): Promise<T>
|
||||
{
|
||||
const res = await fetch(url, { credentials: 'include', ...options });
|
||||
const data = await res.json();
|
||||
|
||||
if(!res.ok || data.error) throw new Error(data.error || 'API error');
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export const useFurniEditor = () =>
|
||||
{
|
||||
const [ items, setItems ] = useState<FurniItem[]>([]);
|
||||
const [ total, setTotal ] = useState(0);
|
||||
const [ page, setPage ] = useState(1);
|
||||
const [ loading, setLoading ] = useState(false);
|
||||
const [ error, setError ] = useState<string | null>(null);
|
||||
const [ selectedItem, setSelectedItem ] = useState<FurniDetail | null>(null);
|
||||
const [ catalogItems, setCatalogItems ] = useState<CatalogRef[]>([]);
|
||||
const [ interactions, setInteractions ] = useState<string[]>([]);
|
||||
const [ furniDataEntry, setFurniDataEntry ] = useState<Record<string, unknown> | null>(null);
|
||||
|
||||
const clearError = useCallback(() => setError(null), []);
|
||||
|
||||
const searchItems = useCallback(async (query: string, type: string, pg: number) =>
|
||||
{
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try
|
||||
{
|
||||
const params = new URLSearchParams({ q: query, limit: '20', page: String(pg) });
|
||||
|
||||
if(type) params.set('type', type);
|
||||
|
||||
const data = await apiFetch<{ items: FurniItem[]; total: number; page: number }>(`${ API_BASE }?${ params }`);
|
||||
|
||||
setItems(data.items);
|
||||
setTotal(data.total);
|
||||
setPage(data.page);
|
||||
}
|
||||
catch(e: any)
|
||||
{
|
||||
setError(e.message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadDetail = useCallback(async (id: number): Promise<boolean> =>
|
||||
{
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try
|
||||
{
|
||||
const data = await apiFetch<{ item: FurniDetail; catalogItems: CatalogRef[]; furniDataEntry: Record<string, unknown> | null }>(`${ API_BASE }/detail?id=${ id }`);
|
||||
|
||||
setSelectedItem(data.item);
|
||||
setCatalogItems(data.catalogItems);
|
||||
setFurniDataEntry(data.furniDataEntry);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch(e: any)
|
||||
{
|
||||
setError(e.message);
|
||||
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateItem = useCallback(async (id: number, fields: Record<string, unknown>) =>
|
||||
{
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try
|
||||
{
|
||||
await apiFetch(`${ API_BASE }/update?id=${ id }`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(fields)
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
catch(e: any)
|
||||
{
|
||||
setError(e.message);
|
||||
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const createItem = useCallback(async (fields: Record<string, unknown>) =>
|
||||
{
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try
|
||||
{
|
||||
const data = await apiFetch<{ id: number }>(`${ API_BASE }`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(fields)
|
||||
});
|
||||
|
||||
return data.id;
|
||||
}
|
||||
catch(e: any)
|
||||
{
|
||||
setError(e.message);
|
||||
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const deleteItem = useCallback(async (id: number) =>
|
||||
{
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try
|
||||
{
|
||||
await apiFetch(`${ API_BASE }/delete?id=${ id }`, { method: 'POST' });
|
||||
|
||||
return true;
|
||||
}
|
||||
catch(e: any)
|
||||
{
|
||||
setError(e.message);
|
||||
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadInteractions = useCallback(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const data = await apiFetch<{ interactions: Array<string | { name: string }> }>(`${ API_BASE }/interactions`);
|
||||
|
||||
setInteractions(data.interactions.map(i => typeof i === 'string' ? i : i.name));
|
||||
}
|
||||
catch {}
|
||||
}, []);
|
||||
|
||||
const loadBySpriteId = useCallback(async (spriteId: number): Promise<boolean> =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const data = await apiFetch<{ id: number }>(`${ API_BASE }/by-sprite?spriteId=${ spriteId }`);
|
||||
|
||||
return await loadDetail(data.id);
|
||||
}
|
||||
catch(e: any)
|
||||
{
|
||||
setError(e.message);
|
||||
|
||||
return false;
|
||||
}
|
||||
}, [ loadDetail ]);
|
||||
|
||||
return {
|
||||
items, total, page, loading, error, clearError,
|
||||
selectedItem, setSelectedItem, catalogItems, furniDataEntry,
|
||||
interactions,
|
||||
searchItems, loadDetail, loadBySpriteId, updateItem, createItem, deleteItem, loadInteractions
|
||||
};
|
||||
};
|
||||
@@ -2,5 +2,6 @@ export * from './useInventoryBadges';
|
||||
export * from './useInventoryBots';
|
||||
export * from './useInventoryFurni';
|
||||
export * from './useInventoryPets';
|
||||
export * from './useInventoryPrefixes';
|
||||
export * from './useInventoryTrade';
|
||||
export * from './useInventoryUnseenTracker';
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { ActivePrefixUpdatedEvent, PrefixReceivedEvent, RequestPrefixesComposer, SetActivePrefixComposer, DeletePrefixComposer, UserPrefixesEvent } from '@nitrots/nitro-renderer';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useBetween } from 'use-between';
|
||||
import { IPrefixItem, SendMessageComposer, UnseenItemCategory } from '../../api';
|
||||
import { useMessageEvent } from '../events';
|
||||
import { useSharedVisibility } from '../useSharedVisibility';
|
||||
import { useInventoryUnseenTracker } from './useInventoryUnseenTracker';
|
||||
|
||||
const useInventoryPrefixesState = () =>
|
||||
{
|
||||
const [ needsUpdate, setNeedsUpdate ] = useState(true);
|
||||
const [ prefixes, setPrefixes ] = useState<IPrefixItem[]>([]);
|
||||
const [ activePrefix, setActivePrefix ] = useState<IPrefixItem | null>(null);
|
||||
const [ selectedPrefix, setSelectedPrefix ] = useState<IPrefixItem | null>(null);
|
||||
const { isVisible = false, activate = null, deactivate = null } = useSharedVisibility();
|
||||
const { isUnseen = null, resetCategory = null } = useInventoryUnseenTracker();
|
||||
|
||||
useMessageEvent<UserPrefixesEvent>(UserPrefixesEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
const newPrefixes: IPrefixItem[] = parser.prefixes.map(p => ({
|
||||
id: p.id,
|
||||
text: p.text,
|
||||
color: p.color,
|
||||
icon: p.icon || '',
|
||||
effect: p.effect || '',
|
||||
active: p.active
|
||||
}));
|
||||
|
||||
setPrefixes(newPrefixes);
|
||||
|
||||
const active = newPrefixes.find(p => p.active) || null;
|
||||
setActivePrefix(active);
|
||||
});
|
||||
|
||||
useMessageEvent<PrefixReceivedEvent>(PrefixReceivedEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
const newPrefix: IPrefixItem = {
|
||||
id: parser.id,
|
||||
text: parser.text,
|
||||
color: parser.color,
|
||||
icon: parser.icon || '',
|
||||
effect: parser.effect || '',
|
||||
active: false
|
||||
};
|
||||
|
||||
setPrefixes(prevValue => [ newPrefix, ...prevValue ]);
|
||||
});
|
||||
|
||||
useMessageEvent<ActivePrefixUpdatedEvent>(ActivePrefixUpdatedEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setPrefixes(prevValue =>
|
||||
{
|
||||
return prevValue.map(p => ({
|
||||
...p,
|
||||
active: p.id === parser.prefixId
|
||||
}));
|
||||
});
|
||||
|
||||
if(parser.prefixId === 0)
|
||||
{
|
||||
setActivePrefix(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
setActivePrefix(prev =>
|
||||
{
|
||||
const found = prefixes.find(p => p.id === parser.prefixId);
|
||||
if(found) return { ...found, active: true };
|
||||
return { id: parser.prefixId, text: parser.text, color: parser.color, icon: parser.icon || '', effect: parser.effect || '', active: true };
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const activatePrefix = (prefixId: number) =>
|
||||
{
|
||||
SendMessageComposer(new SetActivePrefixComposer(prefixId));
|
||||
};
|
||||
|
||||
const deactivatePrefix = () =>
|
||||
{
|
||||
SendMessageComposer(new SetActivePrefixComposer(0));
|
||||
};
|
||||
|
||||
const deletePrefix = (prefixId: number) =>
|
||||
{
|
||||
SendMessageComposer(new DeletePrefixComposer(prefixId));
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!prefixes || !prefixes.length) return;
|
||||
|
||||
setSelectedPrefix(prevValue =>
|
||||
{
|
||||
if(prevValue && prefixes.find(p => p.id === prevValue.id)) return prevValue;
|
||||
return prefixes[0];
|
||||
});
|
||||
}, [ prefixes ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!isVisible) return;
|
||||
|
||||
return () =>
|
||||
{
|
||||
resetCategory(UnseenItemCategory.PREFIX);
|
||||
};
|
||||
}, [ isVisible, resetCategory ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!isVisible || !needsUpdate) return;
|
||||
|
||||
SendMessageComposer(new RequestPrefixesComposer());
|
||||
|
||||
setNeedsUpdate(false);
|
||||
}, [ isVisible, needsUpdate ]);
|
||||
|
||||
return { prefixes, activePrefix, selectedPrefix, setSelectedPrefix, activatePrefix, deactivatePrefix, deletePrefix, activate, deactivate };
|
||||
};
|
||||
|
||||
export const useInventoryPrefixes = () => useBetween(useInventoryPrefixesState);
|
||||
@@ -1,8 +1,8 @@
|
||||
import { CanCreateRoomEventEvent, CantConnectMessageParser, CreateLinkEvent, DoorbellMessageEvent, FavouriteChangedEvent, FavouritesEvent, FlatAccessDeniedMessageEvent, FlatCreatedEvent, FollowFriendMessageComposer, GenericErrorEvent, GetGuestRoomMessageComposer, GetGuestRoomResultEvent, GetSessionDataManager, GetUserEventCatsMessageComposer, GetUserFlatCatsMessageComposer, HabboWebTools, LegacyExternalInterface, NavigatorCategoryDataParser, NavigatorEventCategoryDataParser, NavigatorHomeRoomEvent, NavigatorMetadataEvent, NavigatorOpenRoomCreatorEvent, NavigatorSavedSearch, NavigatorSearchesEvent, NavigatorSearchEvent, NavigatorSearchResultSet, NavigatorTopLevelContext, RoomDataParser, RoomDoorbellAcceptedEvent, RoomEnterErrorEvent, RoomEntryInfoMessageEvent, RoomForwardEvent, RoomScoreEvent, RoomSettingsUpdatedEvent, SecurityLevel, UserEventCatsEvent, UserFlatCatsEvent, UserInfoEvent, UserPermissionsEvent } from '@nitrots/nitro-renderer';
|
||||
import { CanCreateRoomEventEvent, CantConnectMessageParser, CreateLinkEvent, DoorbellMessageEvent, FavouriteChangedEvent, FavouritesEvent, FlatAccessDeniedMessageEvent, FlatCreatedEvent, FollowFriendMessageComposer, GenericErrorEvent, GetGuestRoomMessageComposer, GetGuestRoomResultEvent, GetRoomSessionManager, GetSessionDataManager, GetUserEventCatsMessageComposer, GetUserFlatCatsMessageComposer, HabboWebTools, LegacyExternalInterface, NavigatorCategoryDataParser, NavigatorEventCategoryDataParser, NavigatorHomeRoomEvent, NavigatorMetadataEvent, NavigatorOpenRoomCreatorEvent, NavigatorSavedSearch, NavigatorSearchesEvent, NavigatorSearchEvent, NavigatorSearchResultSet, NavigatorTopLevelContext, NitroEventType, RoomDataParser, RoomDoorbellAcceptedEvent, RoomEnterErrorEvent, RoomEntryInfoMessageEvent, RoomForwardEvent, RoomScoreEvent, RoomSettingsUpdatedEvent, SecurityLevel, UserEventCatsEvent, UserFlatCatsEvent, UserInfoEvent, UserPermissionsEvent } from '@nitrots/nitro-renderer';
|
||||
import { useState } from 'react';
|
||||
import { useBetween } from 'use-between';
|
||||
import { CreateRoomSession, DoorStateType, GetConfigurationValue, INavigatorData, LocalizeText, NotificationAlertType, SendMessageComposer, TryVisitRoom, VisitDesktop } from '../../api';
|
||||
import { useMessageEvent } from '../events';
|
||||
import { useMessageEvent, useNitroEvent } from '../events';
|
||||
import { useNotification } from '../notification';
|
||||
|
||||
const useNavigatorState = () =>
|
||||
@@ -373,6 +373,15 @@ const useNavigatorState = () =>
|
||||
CreateRoomSession(parser.roomId);
|
||||
});
|
||||
|
||||
// When reconnection starts, reset settingsReceived so the login sequence's
|
||||
// NavigatorHomeRoomEvent is treated as a fresh login. Without this, the
|
||||
// prevSettingsReceived check blocks home room navigation after reconnection,
|
||||
// leaving the user stuck on hotel view.
|
||||
useNitroEvent(NitroEventType.SOCKET_RECONNECTING, () =>
|
||||
{
|
||||
setNavigatorData(prevValue => ({ ...prevValue, settingsReceived: false }));
|
||||
});
|
||||
|
||||
useMessageEvent<NavigatorHomeRoomEvent>(NavigatorHomeRoomEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
@@ -397,6 +406,10 @@ const useNavigatorState = () =>
|
||||
return;
|
||||
}
|
||||
|
||||
// If a room session was already restored (from a network disconnect reload),
|
||||
// skip the normal home room navigation to avoid overriding it.
|
||||
if(GetRoomSessionManager().viewerSession) return;
|
||||
|
||||
let forwardType = -1;
|
||||
let forwardId = -1;
|
||||
|
||||
@@ -456,6 +469,11 @@ const useNavigatorState = () =>
|
||||
break;
|
||||
}
|
||||
|
||||
// During reconnection, don't navigate to desktop — the reconnection guard
|
||||
// will handle retrying or cleaning up. Calling VisitDesktop here would
|
||||
// remove the session from the map and send the user to hotel view.
|
||||
if(GetRoomSessionManager().isReconnecting) return;
|
||||
|
||||
VisitDesktop();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './furniture';
|
||||
export * from './useAvatarInfoWidget';
|
||||
export * from './useChatCommandSelector';
|
||||
export * from './useChatInputWidget';
|
||||
export * from './useChatWidget';
|
||||
export * from './useDoorbellWidget';
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import { AvailableCommandsEvent, GetCommunication } from '@nitrots/nitro-renderer';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { CommandDefinition } from '../../../api';
|
||||
import { useMessageEvent } from '../../events';
|
||||
|
||||
const CLIENT_COMMANDS: CommandDefinition[] = [
|
||||
// Effetti stanza
|
||||
{ key: 'shake', description: 'Scuoti la stanza' },
|
||||
{ key: 'rotate', description: 'Ruota la stanza' },
|
||||
{ key: 'zoom', description: 'Zoom stanza' },
|
||||
{ key: 'flip', description: 'Reset zoom' },
|
||||
{ key: 'iddqd', description: 'Reset zoom' },
|
||||
{ key: 'screenshot', description: 'Screenshot stanza' },
|
||||
{ key: 'togglefps', description: 'Toggle FPS' },
|
||||
// Espressioni
|
||||
{ key: 'd', description: 'Ridi (VIP)' },
|
||||
{ key: 'kiss', description: 'Manda un bacio (VIP)' },
|
||||
{ key: 'jump', description: 'Salta (VIP)' },
|
||||
{ key: 'idle', description: 'Vai in idle' },
|
||||
{ key: 'sign', description: 'Mostra cartello' },
|
||||
// Gestione stanza
|
||||
{ key: 'furni', description: 'Furni chooser' },
|
||||
{ key: 'chooser', description: 'User chooser' },
|
||||
{ key: 'floor', description: 'Floor editor' },
|
||||
{ key: 'bcfloor', description: 'Floor editor' },
|
||||
{ key: 'pickall', description: 'Raccogli tutti i furni' },
|
||||
{ key: 'ejectall', description: 'Espelli tutti i furni' },
|
||||
{ key: 'settings', description: 'Impostazioni stanza' },
|
||||
// Info
|
||||
{ key: 'client', description: 'Info client' },
|
||||
{ key: 'nitro', description: 'Info client' },
|
||||
];
|
||||
|
||||
// Module-level cache: cattura i comandi dal server anche prima che React monti
|
||||
let cachedServerCommands: CommandDefinition[] = [];
|
||||
let globalListenerRegistered = false;
|
||||
|
||||
function ensureGlobalListener(): void
|
||||
{
|
||||
if(globalListenerRegistered) return;
|
||||
globalListenerRegistered = true;
|
||||
|
||||
try
|
||||
{
|
||||
const event = new AvailableCommandsEvent((event: AvailableCommandsEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
cachedServerCommands = parser.commands.map(cmd => ({ key: cmd.key, description: cmd.description }));
|
||||
});
|
||||
|
||||
GetCommunication().registerMessageEvent(event);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
// Communication not ready yet, will retry on hook mount
|
||||
globalListenerRegistered = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to register immediately at module load
|
||||
ensureGlobalListener();
|
||||
|
||||
export const useChatCommandSelector = (chatValue: string) =>
|
||||
{
|
||||
const [ serverCommands, setServerCommands ] = useState<CommandDefinition[]>(cachedServerCommands);
|
||||
const [ selectedIndex, setSelectedIndex ] = useState(0);
|
||||
const [ dismissed, setDismissed ] = useState(false);
|
||||
|
||||
// Ensure global listener is registered
|
||||
useEffect(() =>
|
||||
{
|
||||
ensureGlobalListener();
|
||||
|
||||
// If cache already has data (from login), use it
|
||||
if(cachedServerCommands.length > 0 && serverCommands.length === 0)
|
||||
{
|
||||
setServerCommands(cachedServerCommands);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Also listen via React hook for any future updates (e.g. rank change)
|
||||
useMessageEvent<AvailableCommandsEvent>(AvailableCommandsEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
const cmds = parser.commands.map(cmd => ({ key: cmd.key, description: cmd.description }));
|
||||
cachedServerCommands = cmds;
|
||||
setServerCommands(cmds);
|
||||
});
|
||||
|
||||
const allCommands = useMemo(() =>
|
||||
{
|
||||
const merged = [ ...serverCommands ];
|
||||
|
||||
for(const clientCmd of CLIENT_COMMANDS)
|
||||
{
|
||||
if(!merged.some(cmd => cmd.key === clientCmd.key))
|
||||
{
|
||||
merged.push(clientCmd);
|
||||
}
|
||||
}
|
||||
|
||||
return merged.sort((a, b) => a.key.localeCompare(b.key));
|
||||
}, [ serverCommands ]);
|
||||
|
||||
const filterText = useMemo(() =>
|
||||
{
|
||||
if(!chatValue.startsWith(':') || chatValue.includes(' ')) return '';
|
||||
|
||||
return chatValue.slice(1).toLowerCase();
|
||||
}, [ chatValue ]);
|
||||
|
||||
const filteredCommands = useMemo(() =>
|
||||
{
|
||||
if(!filterText && !chatValue.startsWith(':')) return [];
|
||||
|
||||
return allCommands.filter(cmd => cmd.key.toLowerCase().startsWith(filterText));
|
||||
}, [ allCommands, filterText, chatValue ]);
|
||||
|
||||
const isVisible = useMemo(() =>
|
||||
{
|
||||
return chatValue.startsWith(':') && !chatValue.includes(' ') && filteredCommands.length > 0 && !dismissed;
|
||||
}, [ chatValue, filteredCommands, dismissed ]);
|
||||
|
||||
const moveUp = useCallback(() =>
|
||||
{
|
||||
setSelectedIndex(prev => (prev <= 0 ? filteredCommands.length - 1 : prev - 1));
|
||||
}, [ filteredCommands.length ]);
|
||||
|
||||
const moveDown = useCallback(() =>
|
||||
{
|
||||
setSelectedIndex(prev => (prev >= filteredCommands.length - 1 ? 0 : prev + 1));
|
||||
}, [ filteredCommands.length ]);
|
||||
|
||||
const selectCurrent = useCallback((): CommandDefinition | null =>
|
||||
{
|
||||
if(selectedIndex >= 0 && selectedIndex < filteredCommands.length)
|
||||
{
|
||||
return filteredCommands[selectedIndex];
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [ selectedIndex, filteredCommands ]);
|
||||
|
||||
const close = useCallback(() =>
|
||||
{
|
||||
setDismissed(true);
|
||||
}, []);
|
||||
|
||||
// Reset dismissed when chatValue changes to a new command start
|
||||
useEffect(() =>
|
||||
{
|
||||
if(chatValue === ':' || chatValue === '') setDismissed(false);
|
||||
}, [ chatValue ]);
|
||||
|
||||
// Reset selectedIndex when filtered list changes
|
||||
useEffect(() =>
|
||||
{
|
||||
setSelectedIndex(0);
|
||||
}, [ filterText ]);
|
||||
|
||||
return { isVisible, filteredCommands, selectedIndex, setSelectedIndex, moveUp, moveDown, selectCurrent, close };
|
||||
};
|
||||
@@ -116,12 +116,22 @@ const useChatInputWidgetState = () =>
|
||||
|
||||
(async () =>
|
||||
{
|
||||
const image = new Image();
|
||||
try
|
||||
{
|
||||
const imageUrl = await TextureUtils.generateImageUrl(texture);
|
||||
if (!imageUrl) return;
|
||||
|
||||
image.src = await TextureUtils.generateImageUrl(texture);
|
||||
|
||||
const newWindow = window.open('');
|
||||
newWindow.document.write(image.outerHTML);
|
||||
const link = document.createElement('a');
|
||||
link.href = imageUrl;
|
||||
link.download = `room_${ roomSession.roomId }_${ Date.now() }.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.warn('[Screenshot] Failed:', e);
|
||||
}
|
||||
})();
|
||||
return null;
|
||||
case ':pickall':
|
||||
|
||||
@@ -149,6 +149,11 @@ const useChatWidgetState = () =>
|
||||
imageUrl,
|
||||
color);
|
||||
|
||||
chatMessage.prefixText = event.prefixText || '';
|
||||
chatMessage.prefixColor = event.prefixColor || '';
|
||||
chatMessage.prefixIcon = event.prefixIcon || '';
|
||||
chatMessage.prefixEffect = event.prefixEffect || '';
|
||||
|
||||
setChatMessages(prevValue =>
|
||||
{
|
||||
const newValue = [ ...prevValue, chatMessage ];
|
||||
|
||||
Reference in New Issue
Block a user