feat: custom prefix system with effects, emoji picker and per-letter colors

- Catalog page for creating custom prefixes with text, per-letter colors, emoji icon and visual effects
- Emoji picker via @emoji-mart/react with createPortal + Shadow DOM blur fix
- Inventory prefix management (activate/deactivate/delete)
- Chat bubble rendering with multi-color prefix and effect support
- Prefix utilities (getPrefixEffectStyle, parsePrefixColors, PREFIX_EFFECT_KEYFRAMES)
- All UI text in English
This commit is contained in:
simoleo89
2026-03-20 17:07:33 +01:00
parent 0b4a5de8df
commit 11543bb64c
16 changed files with 841 additions and 4 deletions
+1
View File
@@ -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';
+126
View File
@@ -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);