import { AddLinkEventTracker, CustomPrefixPurchaseFailedEvent, ILinkEventTracker, PurchaseCatalogPrefixComposer, PurchaseNickIconComposer, PurchasePrefixComposer, RemoveLinkEventTracker, RequestNickIconsComposer, SetActiveNickIconComposer, SetActivePrefixComposer, SetDisplayOrderComposer, UserNickIconsEvent } from '@nitrots/nitro-renderer'; import data from '@emoji-mart/data'; import Picker from '@emoji-mart/react'; import { FC, useEffect, useMemo, useState } from 'react'; import { INickIconItem, IPrefixItem, PRESET_PREFIX_EFFECTS, PRESET_PREFIX_FONTS, SendMessageComposer, getPrefixEffectStyle, getPrefixFontStyle, parsePrefixColors } from '../../api'; import { GetNickIconUrl } from '../../assets/images/user_custom/nick_icons'; import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Text, UserIdentityView } from '../../common'; import { LayoutCurrencyIcon } from '../../common/layout/LayoutCurrencyIcon'; import { useMessageEvent } from '../../hooks'; type CustomizeTab = 'icons' | 'prefix' | 'settings'; type PrefixSubTab = 'library' | 'custom'; interface ICatalogPrefixItem extends IPrefixItem { points: number; pointsType: number; owned: boolean; ownedPrefixId: number; } interface ICombinedPrefixItem extends IPrefixItem { points: number; pointsType: number; owned: boolean; ownedPrefixId: number; } const ORDER_LABELS: Record = { 'icon-prefix-name': 'Icon / Prefix / Name', 'prefix-icon-name': 'Prefix / Icon / Name', 'name-icon-prefix': 'Name / Icon / Prefix', 'name-prefix-icon': 'Name / Prefix / Icon', 'icon-name-prefix': 'Icon / Name / Prefix', 'prefix-name-icon': 'Prefix / Name / Icon' }; const PRESET_COLORS: string[] = [ '#D62828', '#E85D04', '#F77F00', '#2A9D8F', '#0077B6', '#4361EE', '#6A4C93', '#C1121F', '#B5179E', '#3A86FF', '#3F8E00', '#8D5524' ]; export const CustomizeNickIconView: FC<{}> = () => { const [ isVisible, setIsVisible ] = useState(false); const [ isLoading, setIsLoading ] = useState(false); const [ activeTab, setActiveTab ] = useState('icons'); const [ activePrefixSubTab, setActivePrefixSubTab ] = useState('library'); const [ iconItems, setIconItems ] = useState([]); const [ prefixItems, setPrefixItems ] = useState([]); const [ catalogPrefixes, setCatalogPrefixes ] = useState([]); const [ displayOrder, setDisplayOrder ] = useState('icon-prefix-name'); const [ customPrefixMaxLength, setCustomPrefixMaxLength ] = useState(15); const [ customPrefixPriceCredits, setCustomPrefixPriceCredits ] = useState(0); const [ customPrefixPricePoints, setCustomPrefixPricePoints ] = useState(0); const [ customPrefixPointsType, setCustomPrefixPointsType ] = useState(0); const [ customPrefixFontPriceCredits, setCustomPrefixFontPriceCredits ] = useState(0); const [ customPrefixFontPricePoints, setCustomPrefixFontPricePoints ] = useState(0); const [ customPrefixFontPointsType, setCustomPrefixFontPointsType ] = useState(0); const [ customPrefixText, setCustomPrefixText ] = useState(''); const [ customPrefixColor, setCustomPrefixColor ] = useState('#FFFFFF'); const [ customPrefixIcon, setCustomPrefixIcon ] = useState(''); const [ customPrefixEffect, setCustomPrefixEffect ] = useState(''); const [ customPrefixFont, setCustomPrefixFont ] = useState(''); const [ showEmojiPicker, setShowEmojiPicker ] = useState(false); useMessageEvent(CustomPrefixPurchaseFailedEvent, () => { setIsLoading(false); setIsVisible(false); }); useMessageEvent(UserNickIconsEvent, event => { const parser = event.getParser(); setIconItems(parser.nickIcons.map(icon => ({ id: icon.id, iconKey: icon.iconKey, displayName: icon.displayName, points: icon.points, pointsType: icon.pointsType, owned: icon.owned, active: icon.active }))); setPrefixItems(parser.ownedPrefixes.map(prefix => ({ id: prefix.id, displayName: prefix.displayName, text: prefix.text, color: prefix.color, icon: prefix.icon || '', effect: prefix.effect || '', font: prefix.font || '', active: prefix.active, isCustom: prefix.isCustom, points: prefix.points, pointsType: prefix.pointsType, catalogPrefixId: prefix.catalogPrefixId }))); setCatalogPrefixes(parser.prefixCatalog.map(prefix => ({ id: prefix.id, displayName: prefix.displayName, text: prefix.text, color: prefix.color, icon: prefix.icon || '', effect: prefix.effect || '', font: prefix.font || '', active: prefix.active, points: prefix.points, pointsType: prefix.pointsType, owned: prefix.owned, ownedPrefixId: prefix.ownedPrefixId }))); setDisplayOrder(parser.displayOrder || 'icon-prefix-name'); setCustomPrefixMaxLength(parser.customPrefixMaxLength || 15); setCustomPrefixPriceCredits(parser.customPrefixPriceCredits || 0); setCustomPrefixPricePoints(parser.customPrefixPricePoints || 0); setCustomPrefixPointsType(parser.customPrefixPointsType || 0); setCustomPrefixFontPriceCredits(parser.customPrefixFontPriceCredits || 0); setCustomPrefixFontPricePoints(parser.customPrefixFontPricePoints || 0); setCustomPrefixFontPointsType(parser.customPrefixFontPointsType || 0); setIsLoading(false); }); useEffect(() => { const linkTracker: ILinkEventTracker = { linkReceived: (url: string) => { const parts = url.split('/'); if(parts.length < 2) return; switch(parts[1]) { case 'show': setIsVisible(true); return; case 'hide': setIsVisible(false); return; case 'toggle': setIsVisible(previousValue => !previousValue); return; } }, eventUrlPrefix: 'customize/' }; AddLinkEventTracker(linkTracker); return () => RemoveLinkEventTracker(linkTracker); }, []); useEffect(() => { if(!isVisible) return; setIsLoading(true); SendMessageComposer(new RequestNickIconsComposer()); }, [ isVisible ]); const activeIcon = useMemo(() => iconItems.find(item => item.active) || null, [ iconItems ]); const activePrefix = useMemo(() => prefixItems.find(item => item.active) || null, [ prefixItems ]); const combinedPrefixes = useMemo(() => { const ownedByCatalogId = new Map(); for(const prefix of prefixItems) { if(prefix.catalogPrefixId && (prefix.catalogPrefixId > 0)) ownedByCatalogId.set(prefix.catalogPrefixId, prefix); } const catalogEntries: ICombinedPrefixItem[] = catalogPrefixes.map(prefix => { const ownedPrefix = ownedByCatalogId.get(prefix.id); return { id: ownedPrefix?.id || prefix.id, displayName: ownedPrefix?.displayName || prefix.displayName, text: ownedPrefix?.text || prefix.text, color: ownedPrefix?.color || prefix.color, icon: ownedPrefix?.icon || prefix.icon, effect: ownedPrefix?.effect || prefix.effect, font: ownedPrefix?.font || prefix.font, active: ownedPrefix?.active || prefix.active, isCustom: false, points: prefix.points, pointsType: prefix.pointsType, catalogPrefixId: prefix.id, owned: prefix.owned || !!ownedPrefix, ownedPrefixId: prefix.ownedPrefixId || ownedPrefix?.id || 0 }; }); const customEntries: ICombinedPrefixItem[] = prefixItems .filter(prefix => !prefix.catalogPrefixId || (prefix.catalogPrefixId <= 0)) .map(prefix => ({ id: prefix.id, displayName: prefix.displayName, text: prefix.text, color: prefix.color, icon: prefix.icon, effect: prefix.effect, font: prefix.font || '', active: prefix.active, isCustom: true, points: prefix.points || customPrefixPricePoints, pointsType: prefix.pointsType || customPrefixPointsType, catalogPrefixId: 0, owned: true, ownedPrefixId: prefix.id })); return [ ...catalogEntries, ...customEntries ]; }, [ catalogPrefixes, customPrefixPointsType, customPrefixPricePoints, prefixItems ]); const selectedEffectOption = useMemo(() => PRESET_PREFIX_EFFECTS.find(effect => effect.id === customPrefixEffect) || PRESET_PREFIX_EFFECTS[0], [ customPrefixEffect ]); const selectedFontOption = useMemo(() => PRESET_PREFIX_FONTS.find(font => font.id === customPrefixFont) || PRESET_PREFIX_FONTS[0], [ customPrefixFont ]); const basicEffects = useMemo(() => PRESET_PREFIX_EFFECTS.filter(effect => effect.tier === 'basic'), []); const premiumEffects = useMemo(() => PRESET_PREFIX_EFFECTS.filter(effect => effect.tier === 'premium'), []); const basicFonts = useMemo(() => PRESET_PREFIX_FONTS.filter(font => font.tier === 'basic'), []); const premiumFonts = useMemo(() => PRESET_PREFIX_FONTS.filter(font => font.tier === 'premium'), []); const prefixPreviewColors = useMemo(() => parsePrefixColors(customPrefixText || 'Preview', customPrefixColor || '#FFFFFF'), [ customPrefixText, customPrefixColor ]); const customPrefixPreviewStyle = useMemo(() => getPrefixEffectStyle(customPrefixEffect, prefixPreviewColors[0] || '#FFFFFF'), [ customPrefixEffect, prefixPreviewColors ]); const customPrefixFontStyle = useMemo(() => getPrefixFontStyle(customPrefixFont), [ customPrefixFont ]); const customPrefixTotalCredits = useMemo(() => customPrefixPriceCredits + (customPrefixFont ? customPrefixFontPriceCredits : 0), [ customPrefixFont, customPrefixFontPriceCredits, customPrefixPriceCredits ]); const customPrefixTotalPoints = useMemo(() => customPrefixPricePoints + ((customPrefixFont && (customPrefixFontPointsType === customPrefixPointsType)) ? customPrefixFontPricePoints : 0), [ customPrefixFont, customPrefixFontPointsType, customPrefixFontPricePoints, customPrefixPointsType, customPrefixPricePoints ]); const customPrefixIsValid = useMemo(() => { const trimmed = customPrefixText.trim(); if(!trimmed.length || (trimmed.length > customPrefixMaxLength)) return false; return customPrefixColor.split(',').every(color => /^#[0-9A-Fa-f]{6}$/.test(color)); }, [ customPrefixColor, customPrefixMaxLength, customPrefixText ]); const refreshCustomizeData = () => { setIsLoading(true); SendMessageComposer(new RequestNickIconsComposer()); }; const handleIconAction = (item: INickIconItem) => { setIsLoading(true); if(!item.owned) { SendMessageComposer(new PurchaseNickIconComposer(item.iconKey)); return; } SendMessageComposer(new SetActiveNickIconComposer(item.active ? 0 : item.id)); }; const handleCombinedPrefixAction = (item: ICombinedPrefixItem) => { setIsLoading(true); if(item.owned) { SendMessageComposer(new SetActivePrefixComposer(item.active ? 0 : item.ownedPrefixId)); return; } SendMessageComposer(new PurchaseCatalogPrefixComposer(item.catalogPrefixId || item.id)); }; const handleCustomPrefixPurchase = () => { if(!customPrefixIsValid) return; setIsLoading(true); SendMessageComposer(new PurchasePrefixComposer(customPrefixText.trim(), customPrefixColor, customPrefixIcon, customPrefixEffect, customPrefixFont)); }; const handleDisplayOrderChange = (nextDisplayOrder: string) => { if(nextDisplayOrder === displayOrder) return; setDisplayOrder(nextDisplayOrder); setIsLoading(true); SendMessageComposer(new SetDisplayOrderComposer(nextDisplayOrder)); }; if(!isVisible) return null; return ( setIsVisible(false) } /> setActiveTab('icons') }> Icons setActiveTab('prefix') }> Prefix setActiveTab('settings') }> Settings
Live preview
{ activeTab === 'icons' && <>
Choose the icon shown in your bubble identity.
{ iconItems.map(item => { const iconUrl = GetNickIconUrl(item.iconKey); return (
{ item.active && Active } {
{ item.owned ? (item.active ? 'Owned - Active' : 'Owned') : 'Locked' } { item.displayName || `Icon #${ item.iconKey }` } { item.points }
); }) }
} { activeTab === 'prefix' &&
{ activePrefixSubTab === 'library' && <>
Choose a preset or custom prefix for your bubble identity.
{ combinedPrefixes.map(item => (
{ item.active && Active }
{ item.owned ? (item.active ? 'Owned - Active' : 'Owned') : 'Locked' } { item.displayName || item.text }{ item.isCustom ? ' - Custom' : '' } { item.points }
)) }
} { activePrefixSubTab === 'custom' &&
Custom prefix
setCustomPrefixText(event.target.value) } /> { customPrefixText.length }/{ customPrefixMaxLength }
{ !!customPrefixIcon && }
Safe colors only, chosen to stay readable on both light and dark backgrounds.
{ PRESET_COLORS.map(color => ( )) }
Effect
{ selectedEffectOption.icon } { selectedEffectOption.label }
{ selectedEffectOption.tier }
Font
{ selectedFontOption.label }
{ selectedFontOption.tier }
{ !!customPrefixFont &&
Premium fonts add an extra price on top of the custom prefix.
}
{ customPrefixTotalCredits > 0 && { customPrefixTotalCredits } credits } { customPrefixTotalPoints > 0 && { customPrefixTotalPoints } } { !!customPrefixFont && (customPrefixFontPointsType !== customPrefixPointsType) && (customPrefixFontPricePoints > 0) && { customPrefixFontPricePoints } }
}
} { activeTab === 'settings' &&
Display order
{ Object.entries(ORDER_LABELS).map(([ key, label ]) => ( )) }
Refresh data
Use this tab to control how your icon, prefix and username are ordered in bubbles, profile and infostand.
}
{ showEmojiPicker && <>
setShowEmojiPicker(false) } />
{ setCustomPrefixIcon(emoji.native); setShowEmojiPicker(false); } } previewPosition="none" set="native" theme="dark" />
} ); };