mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
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:
@@ -0,0 +1,9 @@
|
||||
export interface IPrefixItem
|
||||
{
|
||||
id: number;
|
||||
text: string;
|
||||
color: string;
|
||||
icon: string;
|
||||
effect: string;
|
||||
active: boolean;
|
||||
}
|
||||
@@ -6,4 +6,5 @@ export class UnseenItemCategory
|
||||
public static BADGE: number = 4;
|
||||
public static BOT: number = 5;
|
||||
public static GAMES: number = 6;
|
||||
public static PREFIX: number = 7;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ export * from './GroupItem';
|
||||
export * from './IBotItem';
|
||||
export * from './IFurnitureItem';
|
||||
export * from './IPetItem';
|
||||
export * from './IPrefixItem';
|
||||
export * from './IUnseenItemTracker';
|
||||
export * from './InventoryUtilities';
|
||||
export * from './PetUtilities';
|
||||
|
||||
@@ -7,6 +7,10 @@ export class ChatBubbleMessage
|
||||
public height: number = 0;
|
||||
public elementRef: HTMLDivElement = null;
|
||||
public skipMovement: boolean = false;
|
||||
public prefixText: string = '';
|
||||
public prefixColor: string = '';
|
||||
public prefixIcon: string = '';
|
||||
public prefixEffect: string = '';
|
||||
|
||||
private _top: number = 0;
|
||||
private _left: number = 0;
|
||||
|
||||
@@ -7,6 +7,7 @@ export * from './AvatarInfoUser';
|
||||
export * from './AvatarInfoUtilities';
|
||||
export * from './BotSkillsEnum';
|
||||
export * from './ChatBubbleMessage';
|
||||
export * from './CommandDefinition';
|
||||
export * from './ChatBubbleUtilities';
|
||||
export * from './ChatMessageTypeEnum';
|
||||
export * from './DimmerFurnitureWidgetPresetItem';
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
export const PRESET_PREFIX_EFFECTS: { id: string; label: string; icon: string }[] = [
|
||||
{ id: '', label: 'None', icon: '—' },
|
||||
{ id: 'glow', label: 'Glow', icon: '✨' },
|
||||
{ id: 'shadow', label: 'Shadow', icon: '🌑' },
|
||||
{ id: 'italic', label: 'Italic', icon: '𝑰' },
|
||||
{ id: 'outline', label: 'Outline', icon: '🔲' },
|
||||
{ id: 'pulse', label: 'Pulse', icon: '💫' },
|
||||
{ id: 'bold-glow', label: 'Neon', icon: '💡' },
|
||||
];
|
||||
|
||||
export const parsePrefixColors = (text: string, colorStr: string): string[] =>
|
||||
{
|
||||
if(!colorStr || !text) return [];
|
||||
|
||||
const colors = colorStr.split(',');
|
||||
return [ ...text ].map((_, i) => colors[Math.min(i, colors.length - 1)]);
|
||||
};
|
||||
|
||||
export const getPrefixEffectStyle = (effect: string, color?: string): Record<string, string | number> =>
|
||||
{
|
||||
const baseColor = color || '#FFFFFF';
|
||||
|
||||
switch(effect)
|
||||
{
|
||||
case 'glow':
|
||||
return { textShadow: `0 0 6px ${ baseColor }, 0 0 12px ${ baseColor }80` };
|
||||
case 'shadow':
|
||||
return { textShadow: '2px 2px 4px rgba(0,0,0,0.7), 1px 1px 2px rgba(0,0,0,0.5)' };
|
||||
case 'italic':
|
||||
return { fontStyle: 'italic' };
|
||||
case 'outline':
|
||||
return {
|
||||
WebkitTextStroke: '0.5px rgba(0,0,0,0.6)',
|
||||
textShadow: '1px 1px 0 rgba(0,0,0,0.3), -1px -1px 0 rgba(0,0,0,0.3), 1px -1px 0 rgba(0,0,0,0.3), -1px 1px 0 rgba(0,0,0,0.3)'
|
||||
};
|
||||
case 'pulse':
|
||||
return { animation: 'prefix-pulse 1.5s ease-in-out infinite' };
|
||||
case 'bold-glow':
|
||||
return {
|
||||
textShadow: `0 0 4px ${ baseColor }, 0 0 8px ${ baseColor }, 0 0 16px ${ baseColor }60`,
|
||||
fontWeight: 900
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const PREFIX_EFFECT_KEYFRAMES = `
|
||||
@keyframes prefix-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
`;
|
||||
@@ -11,6 +11,7 @@ export * from './LocalizeFormattedNumber';
|
||||
export * from './LocalizeShortNumber';
|
||||
export * from './LocalizeText';
|
||||
export * from './PlaySound';
|
||||
export * from './PrefixUtils';
|
||||
export * from './ProductImageUtility';
|
||||
export * from './Randomizer';
|
||||
export * from './RoomChatFormatter';
|
||||
|
||||
@@ -0,0 +1,470 @@
|
||||
import { PurchasePrefixComposer } from '@nitrots/nitro-renderer';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { FC, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { SendMessageComposer, PRESET_PREFIX_EFFECTS, parsePrefixColors, getPrefixEffectStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../../api';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
import data from '@emoji-mart/data';
|
||||
import Picker from '@emoji-mart/react';
|
||||
|
||||
const PRESET_COLORS: string[] = [
|
||||
'#FF0000', '#FF6600', '#FFCC00', '#33CC00', '#00CCFF',
|
||||
'#0066FF', '#9933FF', '#FF33CC', '#FFFFFF', '#CCCCCC',
|
||||
'#999999', '#333333', '#FF9999', '#99FF99', '#9999FF',
|
||||
'#FFD700', '#FF4500', '#00CED1', '#8A2BE2', '#DC143C'
|
||||
];
|
||||
|
||||
export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null, hideNavigation = null } = props;
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
hideNavigation();
|
||||
}, [ page, hideNavigation ]);
|
||||
|
||||
const [ prefixText, setPrefixText ] = useState('');
|
||||
const [ colorMode, setColorMode ] = useState<'single' | 'perLetter'>('single');
|
||||
const [ singleColor, setSingleColor ] = useState('#FFFFFF');
|
||||
const [ letterColors, setLetterColors ] = useState<Record<number, string>>({});
|
||||
const [ selectedLetterIndex, setSelectedLetterIndex ] = useState<number | null>(null);
|
||||
const [ customColorInput, setCustomColorInput ] = useState('#FFFFFF');
|
||||
const [ selectedIcon, setSelectedIcon ] = useState('');
|
||||
const [ showIconPicker, setShowIconPicker ] = useState(false);
|
||||
const [ selectedEffect, setSelectedEffect ] = useState('');
|
||||
const [ purchased, setPurchased ] = useState(false);
|
||||
const pickerContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Inject style into emoji-mart Shadow DOM to remove backdrop-filter blur
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!showIconPicker) return;
|
||||
|
||||
const timer = setTimeout(() =>
|
||||
{
|
||||
const container = pickerContainerRef.current;
|
||||
if(!container) return;
|
||||
|
||||
const emPicker = container.querySelector('em-emoji-picker');
|
||||
if(!emPicker?.shadowRoot) return;
|
||||
|
||||
const existing = emPicker.shadowRoot.querySelector('#no-blur-fix');
|
||||
if(existing) return;
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.id = 'no-blur-fix';
|
||||
style.textContent = `.sticky { backdrop-filter: none !important; -webkit-backdrop-filter: none !important; background-color: rgb(var(--em-rgb-background)) !important; } .menu { backdrop-filter: none !important; -webkit-backdrop-filter: none !important; background-color: rgb(var(--em-rgb-background)) !important; }`;
|
||||
emPicker.shadowRoot.appendChild(style);
|
||||
}, 50);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [ showIconPicker ]);
|
||||
|
||||
const colorString = useMemo(() =>
|
||||
{
|
||||
if(colorMode === 'single') return singleColor;
|
||||
|
||||
if(!prefixText.length) return singleColor;
|
||||
|
||||
return [ ...prefixText ].map((_, i) => letterColors[i] || singleColor).join(',');
|
||||
}, [ colorMode, singleColor, letterColors, prefixText ]);
|
||||
|
||||
const previewColors = useMemo(() =>
|
||||
{
|
||||
return parsePrefixColors(prefixText || '...', colorString || '#FFFFFF');
|
||||
}, [ prefixText, colorString ]);
|
||||
|
||||
const isValid = useMemo(() =>
|
||||
{
|
||||
if(!prefixText.trim().length || prefixText.trim().length > 15) return false;
|
||||
|
||||
if(colorMode === 'single') return /^#[0-9A-Fa-f]{6}$/.test(singleColor);
|
||||
|
||||
const colors = colorString.split(',');
|
||||
return colors.every(c => /^#[0-9A-Fa-f]{6}$/.test(c));
|
||||
}, [ prefixText, colorMode, singleColor, colorString ]);
|
||||
|
||||
const handlePurchase = () =>
|
||||
{
|
||||
if(!isValid) return;
|
||||
|
||||
SendMessageComposer(new PurchasePrefixComposer(prefixText.trim(), colorString, selectedIcon, selectedEffect));
|
||||
setPurchased(true);
|
||||
setTimeout(() => setPurchased(false), 2000);
|
||||
};
|
||||
|
||||
const handleColorSelect = (color: string) =>
|
||||
{
|
||||
if(colorMode === 'single')
|
||||
{
|
||||
setSingleColor(color);
|
||||
setCustomColorInput(color);
|
||||
}
|
||||
else if(selectedLetterIndex !== null)
|
||||
{
|
||||
setLetterColors(prev => ({ ...prev, [selectedLetterIndex]: color }));
|
||||
setCustomColorInput(color);
|
||||
|
||||
// Auto-advance to next letter
|
||||
if(selectedLetterIndex < prefixText.length - 1)
|
||||
{
|
||||
const nextIdx = selectedLetterIndex + 1;
|
||||
setSelectedLetterIndex(nextIdx);
|
||||
setCustomColorInput(letterColors[nextIdx] || singleColor);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCustomColorChange = (value: string) =>
|
||||
{
|
||||
setCustomColorInput(value);
|
||||
if(/^#[0-9A-Fa-f]{6}$/.test(value))
|
||||
{
|
||||
if(colorMode === 'single')
|
||||
{
|
||||
setSingleColor(value);
|
||||
}
|
||||
else if(selectedLetterIndex !== null)
|
||||
{
|
||||
setLetterColors(prev => ({ ...prev, [selectedLetterIndex]: value }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleTextChange = (newText: string) =>
|
||||
{
|
||||
setPrefixText(newText);
|
||||
if(selectedLetterIndex !== null && selectedLetterIndex >= newText.length)
|
||||
{
|
||||
setSelectedLetterIndex(newText.length > 0 ? newText.length - 1 : null);
|
||||
}
|
||||
};
|
||||
|
||||
const applyColorToAll = () =>
|
||||
{
|
||||
if(!prefixText.length) return;
|
||||
|
||||
const newColors: Record<number, string> = {};
|
||||
[ ...prefixText ].forEach((_, i) => { newColors[i] = customColorInput; });
|
||||
setLetterColors(newColors);
|
||||
};
|
||||
|
||||
const hasMultiColor = colorMode === 'perLetter' && previewColors.length > 1 && new Set(previewColors).size > 1;
|
||||
|
||||
const currentActiveColor = colorMode === 'single'
|
||||
? singleColor
|
||||
: (selectedLetterIndex !== null ? (letterColors[selectedLetterIndex] || singleColor) : singleColor);
|
||||
|
||||
const effectStyle = getPrefixEffectStyle(selectedEffect, previewColors[0] || '#FFFFFF');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 h-full overflow-auto p-1">
|
||||
<style>{ PREFIX_EFFECT_KEYFRAMES }</style>
|
||||
|
||||
{ /* Header */ }
|
||||
{ page.localization.getImage(0) &&
|
||||
<img alt="" className="w-full rounded" src={ page.localization.getImage(0) } /> }
|
||||
{ page.localization.getText(0) &&
|
||||
<div className="text-sm mb-1" dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } /> }
|
||||
|
||||
{ /* Live Preview */ }
|
||||
<div className="relative flex items-center justify-center p-4 rounded-lg min-h-[56px]"
|
||||
style={ {
|
||||
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.05), 0 2px 8px rgba(0,0,0,0.3)'
|
||||
} }>
|
||||
<div className="absolute inset-0 rounded-lg opacity-20"
|
||||
style={ { background: 'radial-gradient(ellipse at center, rgba(100,149,237,0.3) 0%, transparent 70%)' } } />
|
||||
<span className="relative text-xl font-bold tracking-wide" style={ effectStyle }>
|
||||
{ selectedIcon && <span className="mr-1">{ selectedIcon }</span> }
|
||||
<span style={ hasMultiColor ? effectStyle : { ...effectStyle, color: previewColors[0] || '#FFFFFF' } }>
|
||||
{'{'}
|
||||
{ hasMultiColor
|
||||
? [ ...(prefixText || '...') ].map((char, i) => (
|
||||
<span key={ i } style={ { color: previewColors[i] || previewColors[previewColors.length - 1], ...getPrefixEffectStyle(selectedEffect, previewColors[i]) } }>{ char }</span>
|
||||
))
|
||||
: (prefixText || '...')
|
||||
}
|
||||
{'}'}
|
||||
</span>
|
||||
</span>
|
||||
<span className="relative ml-2 text-white/80 text-lg font-medium">Username</span>
|
||||
</div>
|
||||
|
||||
{ /* Text + Icon Row */ }
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col gap-0.5 flex-1">
|
||||
<label className="text-[11px] font-bold uppercase tracking-wider opacity-60">Text</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
className="w-full px-3 py-1.5 rounded-md text-sm focus:outline-none transition-all"
|
||||
maxLength={ 15 }
|
||||
placeholder="Enter text..."
|
||||
style={ {
|
||||
background: 'rgba(0,0,0,0.15)',
|
||||
border: '1px solid rgba(0,0,0,0.15)',
|
||||
color: 'inherit'
|
||||
} }
|
||||
type="text"
|
||||
value={ prefixText }
|
||||
onChange={ e => handleTextChange(e.target.value) } />
|
||||
<span className="absolute right-2 top-1/2 -translate-y-1/2 text-[10px] opacity-30 font-mono">
|
||||
{ prefixText.length }/15
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 relative">
|
||||
<label className="text-[11px] font-bold uppercase tracking-wider opacity-60">Icon</label>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
className="flex items-center justify-center gap-1 px-3 py-1.5 rounded-md text-sm transition-all min-w-[70px]"
|
||||
style={ {
|
||||
background: selectedIcon ? 'rgba(59,130,246,0.15)' : 'rgba(0,0,0,0.15)',
|
||||
border: selectedIcon ? '1px solid rgba(59,130,246,0.3)' : '1px solid rgba(0,0,0,0.15)'
|
||||
} }
|
||||
onClick={ () => setShowIconPicker(!showIconPicker) }>
|
||||
{ selectedIcon
|
||||
? <><span className="text-base">{ selectedIcon }</span><span className="text-[10px] opacity-40">▼</span></>
|
||||
: <span className="opacity-40 text-xs">Emoji ▼</span>
|
||||
}
|
||||
</button>
|
||||
{ selectedIcon &&
|
||||
<button
|
||||
className="flex items-center justify-center px-1.5 rounded-md text-xs transition-all"
|
||||
style={ { background: 'rgba(239,68,68,0.15)', border: '1px solid rgba(239,68,68,0.3)' } }
|
||||
title="Remove icon"
|
||||
onClick={ () => setSelectedIcon('') }>
|
||||
✕
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ /* Emoji Picker (emoji-mart) - portaled to body, no backdrop */ }
|
||||
{ showIconPicker && createPortal(
|
||||
<>
|
||||
<div className="fixed inset-0" style={ { zIndex: 9998 } } onClick={ () => setShowIconPicker(false) } />
|
||||
<div ref={ pickerContainerRef } className="fixed rounded-xl overflow-hidden" style={ { zIndex: 9999, top: '50%', left: '50%', transform: 'translate(-50%, -50%)', background: '#2b2f35' } }>
|
||||
<Picker
|
||||
data={ data }
|
||||
locale="en"
|
||||
onEmojiSelect={ (emoji: { native: string }) => { setSelectedIcon(emoji.native); setShowIconPicker(false); } }
|
||||
theme="dark"
|
||||
previewPosition="none"
|
||||
skinTonePosition="search"
|
||||
perLine={ 8 }
|
||||
maxFrequentRows={ 2 }
|
||||
emojiSize={ 22 }
|
||||
emojiButtonSize={ 30 }
|
||||
dynamicWidth={ false }
|
||||
set="native"
|
||||
/>
|
||||
</div>
|
||||
</>,
|
||||
document.body
|
||||
) }
|
||||
|
||||
{ /* Effect Selector */ }
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[11px] font-bold uppercase tracking-wider opacity-60">Effect</label>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{ PRESET_PREFIX_EFFECTS.map(fx => (
|
||||
<button
|
||||
key={ fx.id }
|
||||
className="px-2 py-1 rounded-md text-[11px] font-semibold transition-all"
|
||||
style={ {
|
||||
background: selectedEffect === fx.id ? 'rgba(59,130,246,0.25)' : 'rgba(0,0,0,0.1)',
|
||||
border: selectedEffect === fx.id ? '1px solid rgba(59,130,246,0.4)' : '1px solid rgba(0,0,0,0.1)',
|
||||
opacity: selectedEffect === fx.id ? 1 : 0.7
|
||||
} }
|
||||
onClick={ () => setSelectedEffect(fx.id) }>
|
||||
<span className="mr-0.5">{ fx.icon }</span> { fx.label }
|
||||
</button>
|
||||
)) }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ /* Color Mode Toggle */ }
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[11px] font-bold uppercase tracking-wider opacity-60">Color</label>
|
||||
<div className="flex rounded-md overflow-hidden" style={ { border: '1px solid rgba(0,0,0,0.15)' } }>
|
||||
<button
|
||||
className="flex-1 px-2 py-1.5 text-xs font-bold transition-all"
|
||||
style={ {
|
||||
background: colorMode === 'single' ? 'rgba(59,130,246,0.25)' : 'rgba(0,0,0,0.1)',
|
||||
borderRight: '1px solid rgba(0,0,0,0.1)',
|
||||
opacity: colorMode === 'single' ? 1 : 0.6
|
||||
} }
|
||||
onClick={ () => { setColorMode('single'); setSelectedLetterIndex(null); } }>
|
||||
🎨 Single
|
||||
</button>
|
||||
<button
|
||||
className="flex-1 px-2 py-1.5 text-xs font-bold transition-all"
|
||||
style={ {
|
||||
background: colorMode === 'perLetter' ? 'rgba(59,130,246,0.25)' : 'rgba(0,0,0,0.1)',
|
||||
opacity: colorMode === 'perLetter' ? 1 : 0.6
|
||||
} }
|
||||
onClick={ () => { setColorMode('perLetter'); if(prefixText.length > 0) setSelectedLetterIndex(0); } }>
|
||||
🌈 Per Letter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ /* Per-Letter Selector */ }
|
||||
{ colorMode === 'perLetter' && prefixText.length > 0 && (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] opacity-50">
|
||||
Select a letter, then choose a color. Auto-advances.
|
||||
</span>
|
||||
<button
|
||||
className="text-[10px] px-1.5 py-0.5 rounded transition-all"
|
||||
style={ {
|
||||
background: 'rgba(0,0,0,0.1)',
|
||||
border: '1px solid rgba(0,0,0,0.1)'
|
||||
} }
|
||||
title="Apply current color to all letters"
|
||||
onClick={ applyColorToAll }>
|
||||
Apply to all
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1 p-2 rounded-lg"
|
||||
style={ {
|
||||
background: 'rgba(0,0,0,0.12)',
|
||||
border: '1px solid rgba(0,0,0,0.1)'
|
||||
} }>
|
||||
{ [ ...prefixText ].map((char, i) =>
|
||||
{
|
||||
const charColor = letterColors[i] || singleColor;
|
||||
const isSelected = selectedLetterIndex === i;
|
||||
return (
|
||||
<div
|
||||
key={ i }
|
||||
className="relative flex items-center justify-center cursor-pointer transition-all"
|
||||
style={ {
|
||||
width: '28px',
|
||||
height: '34px',
|
||||
borderRadius: '6px',
|
||||
background: isSelected
|
||||
? 'rgba(59,130,246,0.2)'
|
||||
: 'rgba(0,0,0,0.12)',
|
||||
border: isSelected
|
||||
? '2px solid rgba(59,130,246,0.6)'
|
||||
: '1px solid rgba(0,0,0,0.08)',
|
||||
transform: isSelected ? 'scale(1.15)' : 'scale(1)',
|
||||
zIndex: isSelected ? 10 : 1,
|
||||
boxShadow: isSelected ? '0 0 8px rgba(59,130,246,0.3)' : 'none'
|
||||
} }
|
||||
onClick={ () => { setSelectedLetterIndex(i); setCustomColorInput(charColor); } }>
|
||||
<span className="text-sm font-black" style={ { color: charColor } }>
|
||||
{ char }
|
||||
</span>
|
||||
<div
|
||||
className="absolute bottom-0.5 left-1/2 -translate-x-1/2 rounded-full"
|
||||
style={ {
|
||||
width: '14px',
|
||||
height: '3px',
|
||||
backgroundColor: charColor,
|
||||
boxShadow: `0 0 4px ${ charColor }`
|
||||
} } />
|
||||
</div>
|
||||
);
|
||||
}) }
|
||||
</div>
|
||||
</div>
|
||||
) }
|
||||
|
||||
{ /* Color Palette */ }
|
||||
<div className="flex flex-col gap-1">
|
||||
{ colorMode === 'perLetter' && selectedLetterIndex !== null &&
|
||||
<span className="text-[10px] opacity-50 italic">
|
||||
Selected letter: "{ prefixText[selectedLetterIndex] || '' }"
|
||||
</span>
|
||||
}
|
||||
<div className="grid grid-cols-10 gap-[3px]">
|
||||
{ PRESET_COLORS.map((color, idx) =>
|
||||
{
|
||||
const isActive = currentActiveColor === color;
|
||||
return (
|
||||
<div
|
||||
key={ idx }
|
||||
className="cursor-pointer transition-all"
|
||||
style={ {
|
||||
width: '100%',
|
||||
aspectRatio: '1',
|
||||
borderRadius: '5px',
|
||||
backgroundColor: color,
|
||||
border: isActive ? '2px solid #fff' : '1px solid rgba(0,0,0,0.15)',
|
||||
boxShadow: isActive ? `0 0 6px ${ color }, 0 0 0 1px rgba(0,0,0,0.2)` : 'inset 0 1px 0 rgba(255,255,255,0.2)',
|
||||
transform: isActive ? 'scale(1.2)' : 'scale(1)',
|
||||
zIndex: isActive ? 5 : 1
|
||||
} }
|
||||
onClick={ () => handleColorSelect(color) } />
|
||||
);
|
||||
}) }
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<label
|
||||
className="relative cursor-pointer"
|
||||
style={ {
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: customColorInput,
|
||||
border: '2px solid rgba(0,0,0,0.2)',
|
||||
boxShadow: `0 0 6px ${ customColorInput }40, inset 0 1px 0 rgba(255,255,255,0.3)`
|
||||
} }>
|
||||
<input
|
||||
className="absolute inset-0 opacity-0 cursor-pointer"
|
||||
style={ { width: '100%', height: '100%' } }
|
||||
type="color"
|
||||
value={ customColorInput }
|
||||
onChange={ e => handleColorSelect(e.target.value) } />
|
||||
</label>
|
||||
<input
|
||||
className="flex-1 px-2 py-0.5 text-xs font-mono focus:outline-none transition-all"
|
||||
maxLength={ 7 }
|
||||
placeholder="#FFFFFF"
|
||||
style={ {
|
||||
background: 'rgba(0,0,0,0.15)',
|
||||
border: '1px solid rgba(0,0,0,0.1)',
|
||||
color: 'inherit',
|
||||
maxWidth: '80px',
|
||||
borderRadius: '5px'
|
||||
} }
|
||||
type="text"
|
||||
value={ customColorInput }
|
||||
onChange={ e => handleCustomColorChange(e.target.value) } />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ /* Purchase Footer */ }
|
||||
<div className="flex items-center justify-between mt-auto pt-2"
|
||||
style={ { borderTop: '1px solid rgba(0,0,0,0.1)' } }>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-xs opacity-60">Price:</span>
|
||||
<span className="text-sm font-bold">5 Credits</span>
|
||||
</div>
|
||||
<button
|
||||
className="px-5 py-1.5 rounded-md text-sm font-bold transition-all"
|
||||
disabled={ !isValid || purchased }
|
||||
style={ {
|
||||
background: !isValid
|
||||
? 'rgba(0,0,0,0.1)'
|
||||
: purchased
|
||||
? 'linear-gradient(135deg, #22c55e, #16a34a)'
|
||||
: 'linear-gradient(135deg, #3b82f6, #2563eb)',
|
||||
color: !isValid ? 'rgba(0,0,0,0.3)' : '#fff',
|
||||
cursor: !isValid ? 'not-allowed' : 'pointer',
|
||||
border: !isValid ? '1px solid rgba(0,0,0,0.1)' : 'none',
|
||||
boxShadow: isValid && !purchased ? '0 2px 8px rgba(59,130,246,0.3)' : 'none',
|
||||
borderRadius: '6px'
|
||||
} }
|
||||
onClick={ handlePurchase }>
|
||||
{ purchased ? '✓ Purchased!' : 'Purchase' }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { ICatalogPage } from '../../../../../api';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
import { CatalogLayoutBadgeDisplayView } from './CatalogLayoutBadgeDisplayView';
|
||||
import { CatalogLayoutColorGroupingView } from './CatalogLayoutColorGroupingView';
|
||||
import { CatalogLayoutCustomPrefixView } from './CatalogLayoutCustomPrefixView';
|
||||
import { CatalogLayoutDefaultView } from './CatalogLayoutDefaultView';
|
||||
import { CatalogLayouGuildCustomFurniView } from './CatalogLayoutGuildCustomFurniView';
|
||||
import { CatalogLayouGuildForumView } from './CatalogLayoutGuildForumView';
|
||||
@@ -72,6 +73,8 @@ export const GetCatalogLayout = (page: ICatalogPage, hideNavigation: () => void)
|
||||
return <CatalogLayoutColorGroupingView { ...layoutProps } />;
|
||||
case 'soundmachine':
|
||||
return <CatalogLayoutSoundMachineView { ...layoutProps } />;
|
||||
case 'custom_prefix':
|
||||
return <CatalogLayoutCustomPrefixView { ...layoutProps } />;
|
||||
case 'bots':
|
||||
case 'default_3x3':
|
||||
default:
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AddLinkEventTracker, BadgePointLimitsEvent, GetLocalizationManager, Get
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { GroupItem, LocalizeText, UnseenItemCategory, isObjectMoverRequested, setObjectMoverRequested } from '../../api';
|
||||
import { NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
|
||||
import { useInventoryBadges, useInventoryFurni, useInventoryTrade, useInventoryUnseenTracker, useMessageEvent, useNitroEvent } from '../../hooks';
|
||||
import { useInventoryBadges, useInventoryFurni, useInventoryPrefixes, useInventoryTrade, useInventoryUnseenTracker, useMessageEvent, useNitroEvent } from '../../hooks';
|
||||
import { InventoryCategoryFilterView } from './views/InventoryCategoryFilterView';
|
||||
import { InventoryBadgeView } from './views/badge/InventoryBadgeView';
|
||||
import { InventoryBotView } from './views/bot/InventoryBotView';
|
||||
@@ -10,13 +10,15 @@ import { InventoryFurnitureDeleteView } from './views/furniture/InventoryFurnitu
|
||||
import { InventoryFurnitureView } from './views/furniture/InventoryFurnitureView';
|
||||
import { InventoryTradeView } from './views/furniture/InventoryTradeView';
|
||||
import { InventoryPetView } from './views/pet/InventoryPetView';
|
||||
import { InventoryPrefixView } from './views/prefix/InventoryPrefixView';
|
||||
|
||||
const TAB_FURNITURE: string = 'inventory.furni';
|
||||
const TAB_BOTS: string = 'inventory.bots';
|
||||
const TAB_PETS: string = 'inventory.furni.tab.pets';
|
||||
const TAB_BADGES: string = 'inventory.badges';
|
||||
const TABS = [ TAB_FURNITURE, TAB_PETS, TAB_BADGES, TAB_BOTS ];
|
||||
const UNSEEN_CATEGORIES = [ UnseenItemCategory.FURNI, UnseenItemCategory.PET, UnseenItemCategory.BADGE, UnseenItemCategory.BOT ];
|
||||
const TAB_PREFIXES: string = 'inventory.prefixes';
|
||||
const TABS = [ TAB_FURNITURE, TAB_PETS, TAB_BADGES, TAB_PREFIXES, TAB_BOTS ];
|
||||
const UNSEEN_CATEGORIES = [ UnseenItemCategory.FURNI, UnseenItemCategory.PET, UnseenItemCategory.BADGE, UnseenItemCategory.PREFIX, UnseenItemCategory.BOT ];
|
||||
|
||||
export const InventoryView: FC<{}> = props =>
|
||||
{
|
||||
@@ -165,6 +167,8 @@ export const InventoryView: FC<{}> = props =>
|
||||
<InventoryPetView roomPreviewer={ roomPreviewer } roomSession={ roomSession } /> }
|
||||
{ (currentTab === TAB_BADGES) &&
|
||||
<InventoryBadgeView filteredBadgeCodes={ filteredBadgeCodes } /> }
|
||||
{ (currentTab === TAB_PREFIXES) &&
|
||||
<InventoryPrefixView /> }
|
||||
{ (currentTab === TAB_BOTS) &&
|
||||
<InventoryBotView roomPreviewer={ roomPreviewer } roomSession={ roomSession } /> }
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FaTrashAlt } from 'react-icons/fa';
|
||||
import { IPrefixItem, LocalizeText, parsePrefixColors, getPrefixEffectStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../api';
|
||||
import { useInventoryPrefixes, useNotification } from '../../../../hooks';
|
||||
import { NitroButton } from '../../../../layout';
|
||||
|
||||
const PrefixPreview: FC<{ text: string; color: string; icon: string; effect?: string; className?: string; textSize?: string }> = ({ text, color, icon, effect = '', className = '', textSize = 'text-sm' }) =>
|
||||
{
|
||||
const colors = parsePrefixColors(text, color);
|
||||
const hasMultiColor = colors.length > 1 && new Set(colors).size > 1;
|
||||
const fxStyle = getPrefixEffectStyle(effect, colors[0] || '#FFFFFF');
|
||||
|
||||
return (
|
||||
<span className={ `font-bold ${ textSize } ${ className }` } style={ fxStyle }>
|
||||
{ effect === 'pulse' && <style>{ PREFIX_EFFECT_KEYFRAMES }</style> }
|
||||
{ icon && <span className="mr-0.5">{ icon }</span> }
|
||||
<span style={ hasMultiColor ? fxStyle : { ...fxStyle, color: colors[0] || '#FFFFFF' } }>
|
||||
{'{'}
|
||||
{ hasMultiColor
|
||||
? [ ...text ].map((char, i) => (
|
||||
<span key={ i } style={ { color: colors[i] || colors[colors.length - 1], ...getPrefixEffectStyle(effect, colors[i]) } }>{ char }</span>
|
||||
))
|
||||
: text
|
||||
}
|
||||
{'}'}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const PrefixItemView: FC<{
|
||||
prefix: IPrefixItem;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
}> = ({ prefix, isSelected, onClick }) =>
|
||||
{
|
||||
return (
|
||||
<div
|
||||
className={ `flex items-center justify-center rounded-md border-2 cursor-pointer p-2 transition-colors
|
||||
${ isSelected ? 'border-card-grid-item-active bg-card-grid-item-active' : 'border-card-grid-item-border bg-card-grid-item' }
|
||||
${ prefix.active ? 'ring-2 ring-green-400' : '' }` }
|
||||
onClick={ onClick }>
|
||||
<PrefixPreview className="truncate" color={ prefix.color } effect={ prefix.effect } icon={ prefix.icon } text={ prefix.text } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const InventoryPrefixView: FC<{}> = () =>
|
||||
{
|
||||
const [ isVisible, setIsVisible ] = useState(false);
|
||||
const { prefixes = [], activePrefix = null, selectedPrefix = null, setSelectedPrefix = null, activatePrefix = null, deactivatePrefix = null, deletePrefix = null, activate = null, deactivate = null } = useInventoryPrefixes();
|
||||
const { showConfirm = null } = useNotification();
|
||||
|
||||
const attemptDeletePrefix = () =>
|
||||
{
|
||||
if(!selectedPrefix) return;
|
||||
|
||||
showConfirm(
|
||||
`Are you sure you want to delete the prefix {${selectedPrefix.text}}?`,
|
||||
() => deletePrefix(selectedPrefix.id),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
LocalizeText('inventory.delete.confirm_delete.title')
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!isVisible) return;
|
||||
|
||||
const id = activate();
|
||||
|
||||
return () => deactivate(id);
|
||||
}, [ isVisible, activate, deactivate ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setIsVisible(true);
|
||||
|
||||
return () => setIsVisible(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="grid h-full grid-cols-12 gap-2">
|
||||
<div className="flex flex-col col-span-7 gap-1 overflow-auto">
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{ prefixes.map(prefix => (
|
||||
<PrefixItemView
|
||||
key={ prefix.id }
|
||||
isSelected={ selectedPrefix?.id === prefix.id }
|
||||
prefix={ prefix }
|
||||
onClick={ () => setSelectedPrefix(prefix) } />
|
||||
)) }
|
||||
</div>
|
||||
{ (!prefixes || prefixes.length === 0) &&
|
||||
<div className="flex items-center justify-center h-full text-sm opacity-50">
|
||||
{ LocalizeText('inventory.empty.title') }
|
||||
</div> }
|
||||
</div>
|
||||
<div className="flex flex-col justify-between col-span-5 overflow-auto">
|
||||
{ activePrefix &&
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm truncate min-h-[1.25rem] leading-5">Active prefix</span>
|
||||
<div className="flex items-center justify-center p-3 rounded-md border-2 border-green-400 bg-card-grid-item">
|
||||
<PrefixPreview color={ activePrefix.color } effect={ activePrefix.effect } icon={ activePrefix.icon } text={ activePrefix.text } textSize="text-lg" />
|
||||
</div>
|
||||
</div> }
|
||||
{ !activePrefix &&
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm truncate min-h-[1.25rem] leading-5">Active prefix</span>
|
||||
<div className="flex items-center justify-center p-3 rounded-md border-2 border-dashed border-card-grid-item-border bg-card-grid-item opacity-50">
|
||||
<span className="text-sm">No active prefix</span>
|
||||
</div>
|
||||
</div> }
|
||||
{ !!selectedPrefix &&
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
<div className="flex items-center justify-center gap-2 p-2 rounded bg-card-grid-item">
|
||||
<PrefixPreview color={ selectedPrefix.color } effect={ selectedPrefix.effect } icon={ selectedPrefix.icon } text={ selectedPrefix.text } textSize="text-lg" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<NitroButton
|
||||
className="grow"
|
||||
onClick={ () => selectedPrefix.active ? deactivatePrefix() : activatePrefix(selectedPrefix.id) }>
|
||||
{ selectedPrefix.active ? 'Deactivate' : 'Activate' }
|
||||
</NitroButton>
|
||||
{ !selectedPrefix.active &&
|
||||
<NitroButton className="bg-danger! hover:bg-danger/80! p-1" onClick={ attemptDeletePrefix }>
|
||||
<FaTrashAlt className="fa-icon" />
|
||||
</NitroButton> }
|
||||
</div>
|
||||
</div> }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { GetRoomEngine, RoomChatSettings, RoomObjectCategory } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ChatBubbleMessage } from '../../../../api';
|
||||
import { ChatBubbleMessage, parsePrefixColors, getPrefixEffectStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../api';
|
||||
import { useOnClickChat } from '../../../../hooks';
|
||||
|
||||
interface ChatWidgetMessageViewProps
|
||||
@@ -90,6 +90,27 @@ export const ChatWidgetMessageView: FC<ChatWidgetMessageViewProps> = ({
|
||||
) }
|
||||
</div>
|
||||
<div className="chat-content py-[5px] px-[6px] ml-[27px] leading-none min-h-[25px]">
|
||||
{ chat.prefixEffect === 'pulse' && <style>{ PREFIX_EFFECT_KEYFRAMES }</style> }
|
||||
{ chat.prefixText && (() => {
|
||||
const colors = parsePrefixColors(chat.prefixText, chat.prefixColor);
|
||||
const hasMultiColor = colors.length > 1 && new Set(colors).size > 1;
|
||||
const fxStyle = getPrefixEffectStyle(chat.prefixEffect, colors[0] || '#FFFFFF');
|
||||
return (
|
||||
<span className="prefix font-bold mr-1" style={ fxStyle }>
|
||||
{ chat.prefixIcon && <span className="mr-0.5 text-[13px]">{ chat.prefixIcon }</span> }
|
||||
<span style={ hasMultiColor ? fxStyle : { ...fxStyle, color: colors[0] || '#FFFFFF' } }>
|
||||
{'{'}
|
||||
{ hasMultiColor
|
||||
? [ ...chat.prefixText ].map((char, i) => (
|
||||
<span key={ i } style={ { color: colors[i] || colors[colors.length - 1], ...getPrefixEffectStyle(chat.prefixEffect, colors[i]) } }>{ char }</span>
|
||||
))
|
||||
: chat.prefixText
|
||||
}
|
||||
{'}'}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
})() }
|
||||
<b className="username" dangerouslySetInnerHTML={ { __html: `${ chat.username }: ` } } />
|
||||
<span className={ `message${ chat.type === 1 ? ' italic text-[#595959]' : '' }` } dangerouslySetInnerHTML={ { __html: `${ chat.formattedText }` } } onClick={ onClickChat } />
|
||||
</div>
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
export * from './furniture';
|
||||
export * from './useAvatarInfoWidget';
|
||||
export * from './useChatCommandSelector';
|
||||
export * from './useChatInputWidget';
|
||||
export * from './useChatWidget';
|
||||
export * from './useDoorbellWidget';
|
||||
|
||||
@@ -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