Align wired chat limits and formatting help UI

This commit is contained in:
Lorenzune
2026-04-21 08:54:02 +02:00
parent 5e9e3e1e4c
commit e0174e450c
5 changed files with 147 additions and 10 deletions
+43 -1
View File
@@ -40,11 +40,53 @@ const encodeHTML = (str: string) =>
}); });
}; };
const formatTag = (content: string, tag: string, replacement: (value: string) => string) =>
{
const pattern = new RegExp(`\\[${ tag }\\]([\\s\\S]*?)\\[\\/${ tag }\\]`, 'gi');
let previous = '';
let next = content;
let guard = 0;
while((previous !== next) && (guard < 20))
{
previous = next;
next = next.replace(pattern, (match, value) => replacement(value));
guard++;
}
return next;
};
const applyWiredTextMarkup = (content: string) =>
{
const colorStyles: Record<string, string> = {
green: '#008000',
cyan: '#008b8b',
red: '#d60000',
blue: '#005dff',
purple: '#7d31b8'
};
let result = content;
result = formatTag(result, 'b', value => `<strong>${ value }</strong>`);
result = formatTag(result, 'i', value => `<em>${ value }</em>`);
result = formatTag(result, 'u', value => `<u>${ value }</u>`);
Object.entries(colorStyles).forEach(([ tag, color ]) =>
{
result = formatTag(result, tag, value => `<span style="color:${ color }">${ value }</span>`);
});
return result;
};
export const RoomChatFormatter = (content: string) => export const RoomChatFormatter = (content: string) =>
{ {
let result = ''; let result = '';
content = encodeHTML(content); content = encodeHTML(content);
content = applyWiredTextMarkup(content);
//content = (joypixels.shortnameToUnicode(content) as string) //content = (joypixels.shortnameToUnicode(content) as string)
if(!GetConfigurationValue<boolean>('youtube.publish.disabled', false)) if(!GetConfigurationValue<boolean>('youtube.publish.disabled', false))
@@ -84,5 +126,5 @@ export const RoomChatFormatter = (content: string) =>
result = content; result = content;
} }
return result; return result.replace(/\r\n|\r|\n/g, '<br />');
}; };
@@ -1,9 +1,10 @@
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { GetConfigurationValue, LocalizeText, WIRED_STRING_DELIMETER, WiredFurniType } from '../../../../api'; import { LocalizeText, WIRED_STRING_DELIMETER, WiredFurniType } from '../../../../api';
import { Text } from '../../../../common'; import { Text } from '../../../../common';
import { useWired } from '../../../../hooks'; import { useWired } from '../../../../hooks';
import { NitroInput } from '../../../../layout'; import { NitroInput } from '../../../../layout';
import { WiredActionBaseView } from './WiredActionBaseView'; import { WiredActionBaseView } from './WiredActionBaseView';
import { WiredTextCounter, WiredTextFormattingHelp } from '../common/WiredTextFormattingHelp';
import { BOT_SOURCES, WiredSourcesSelector } from '../WiredSourcesSelector'; import { BOT_SOURCES, WiredSourcesSelector } from '../WiredSourcesSelector';
const normalizeBotSource = (value: number, hasBotName = false) => (BOT_SOURCES.some(option => (option.value === value)) ? value : (hasBotName ? 100 : 0)); const normalizeBotSource = (value: number, hasBotName = false) => (BOT_SOURCES.some(option => (option.value === value)) ? value : (hasBotName ? 100 : 0));
@@ -15,6 +16,7 @@ export const WiredActionBotTalkToAvatarView: FC<{}> = props =>
const [ talkMode, setTalkMode ] = useState(-1); const [ talkMode, setTalkMode ] = useState(-1);
const [ botSource, setBotSource ] = useState<number>(100); const [ botSource, setBotSource ] = useState<number>(100);
const { trigger = null, setStringParam = null, setIntParams = null } = useWired(); const { trigger = null, setStringParam = null, setIntParams = null } = useWired();
const maxMessageLength = 100;
const [ userSource, setUserSource ] = useState<number>(() => const [ userSource, setUserSource ] = useState<number>(() =>
{ {
if(trigger?.intData?.length > 1) return trigger.intData[1]; if(trigger?.intData?.length > 1) return trigger.intData[1];
@@ -59,7 +61,14 @@ export const WiredActionBotTalkToAvatarView: FC<{}> = props =>
</div> } </div> }
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.message') }</Text> <Text bold>{ LocalizeText('wiredfurni.params.message') }</Text>
<NitroInput maxLength={ GetConfigurationValue<number>('wired.action.bot.talk.to.avatar.max.length', 64) } type="text" value={ message } onChange={ event => setMessage(event.target.value) } /> <textarea
className="form-control form-control-sm nitro-wired__resizable-textarea"
maxLength={ maxMessageLength }
rows={ 4 }
value={ message }
onChange={ event => setMessage(event.target.value) } />
<WiredTextCounter maxLength={ maxMessageLength } value={ message } />
<WiredTextFormattingHelp />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
@@ -1,9 +1,10 @@
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { GetConfigurationValue, LocalizeText, WIRED_STRING_DELIMETER, WiredFurniType } from '../../../../api'; import { LocalizeText, WIRED_STRING_DELIMETER, WiredFurniType } from '../../../../api';
import { Text } from '../../../../common'; import { Text } from '../../../../common';
import { useWired } from '../../../../hooks'; import { useWired } from '../../../../hooks';
import { NitroInput } from '../../../../layout'; import { NitroInput } from '../../../../layout';
import { WiredActionBaseView } from './WiredActionBaseView'; import { WiredActionBaseView } from './WiredActionBaseView';
import { WiredTextCounter, WiredTextFormattingHelp } from '../common/WiredTextFormattingHelp';
import { BOT_SOURCES, WiredSourcesSelector } from '../WiredSourcesSelector'; import { BOT_SOURCES, WiredSourcesSelector } from '../WiredSourcesSelector';
const normalizeBotSource = (value: number, hasBotName = false) => (BOT_SOURCES.some(option => (option.value === value)) ? value : (hasBotName ? 100 : 0)); const normalizeBotSource = (value: number, hasBotName = false) => (BOT_SOURCES.some(option => (option.value === value)) ? value : (hasBotName ? 100 : 0));
@@ -15,6 +16,7 @@ export const WiredActionBotTalkView: FC<{}> = props =>
const [ talkMode, setTalkMode ] = useState(-1); const [ talkMode, setTalkMode ] = useState(-1);
const [ botSource, setBotSource ] = useState<number>(100); const [ botSource, setBotSource ] = useState<number>(100);
const { trigger = null, setStringParam = null, setIntParams = null } = useWired(); const { trigger = null, setStringParam = null, setIntParams = null } = useWired();
const maxMessageLength = 100;
const save = () => const save = () =>
{ {
@@ -43,7 +45,14 @@ export const WiredActionBotTalkView: FC<{}> = props =>
</div> } </div> }
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.message') }</Text> <Text bold>{ LocalizeText('wiredfurni.params.message') }</Text>
<NitroInput maxLength={ GetConfigurationValue<number>('wired.action.bot.talk.max.length', 64) } type="text" value={ message } onChange={ event => setMessage(event.target.value) } /> <textarea
className="form-control form-control-sm nitro-wired__resizable-textarea"
maxLength={ maxMessageLength }
rows={ 4 }
value={ message }
onChange={ event => setMessage(event.target.value) } />
<WiredTextCounter maxLength={ maxMessageLength } value={ message } />
<WiredTextFormattingHelp />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
@@ -1,13 +1,24 @@
import { FC, useEffect, useMemo, useState } from 'react'; import { FC, useEffect, useMemo, useState } from 'react';
import { GetConfigurationValue, LocalizeText, WiredFurniType } from '../../../../api'; import { LocalizeText, WiredFurniType } from '../../../../api';
import { Text } from '../../../../common'; import { Text } from '../../../../common';
import { useWired } from '../../../../hooks'; import { useWired } from '../../../../hooks';
import { NitroInput } from '../../../../layout';
import { WiredActionBaseView } from './WiredActionBaseView'; import { WiredActionBaseView } from './WiredActionBaseView';
import { WiredTextCounter, WiredTextFormattingHelp } from '../common/WiredTextFormattingHelp';
import { WiredSourcesSelector } from '../WiredSourcesSelector'; import { WiredSourcesSelector } from '../WiredSourcesSelector';
const SHOW_MESSAGE_STYLE_IDS = [ 34, 200, 201, 202, 210, 211, 212, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 250, 251, 252 ]; const SHOW_MESSAGE_STYLE_IDS = [ 34, 200, 201, 202, 210, 211, 212, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 250, 251, 252 ];
const DEFAULT_SHOW_MESSAGE_STYLE_ID = 34; const DEFAULT_SHOW_MESSAGE_STYLE_ID = 34;
const SHOW_MESSAGE_MAX_LENGTH = 200;
const SHOW_MESSAGE_MAX_LINES = 8;
const clampShowMessage = (value: string) =>
{
const normalized = (value ?? '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const lines = normalized.split('\n').slice(0, SHOW_MESSAGE_MAX_LINES);
const joined = lines.join('\n');
return joined.slice(0, SHOW_MESSAGE_MAX_LENGTH);
};
export const WiredActionChatView: FC<{}> = props => export const WiredActionChatView: FC<{}> = props =>
{ {
@@ -21,16 +32,17 @@ export const WiredActionChatView: FC<{}> = props =>
return 0; return 0;
}); });
const bubbleStyleIds = useMemo(() => SHOW_MESSAGE_STYLE_IDS, []); const bubbleStyleIds = useMemo(() => SHOW_MESSAGE_STYLE_IDS, []);
const maxMessageLength = SHOW_MESSAGE_MAX_LENGTH;
const save = () => const save = () =>
{ {
setStringParam(message); setStringParam(clampShowMessage(message));
setIntParams([ userSource, visibilitySelection, bubbleStyle ]); setIntParams([ userSource, visibilitySelection, bubbleStyle ]);
}; };
useEffect(() => useEffect(() =>
{ {
setMessage(trigger.stringData); setMessage(clampShowMessage(trigger.stringData));
if(trigger.intData.length >= 1) setUserSource(trigger.intData[0]); if(trigger.intData.length >= 1) setUserSource(trigger.intData[0]);
else setUserSource(0); else setUserSource(0);
if(trigger.intData.length >= 2) setVisibilitySelection(trigger.intData[1]); if(trigger.intData.length >= 2) setVisibilitySelection(trigger.intData[1]);
@@ -47,7 +59,14 @@ export const WiredActionChatView: FC<{}> = props =>
footer={ <WiredSourcesSelector showUsers={ true } userSource={ userSource } onChangeUsers={ setUserSource } /> }> footer={ <WiredSourcesSelector showUsers={ true } userSource={ userSource } onChangeUsers={ setUserSource } /> }>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.message') }</Text> <Text bold>{ LocalizeText('wiredfurni.params.message') }</Text>
<NitroInput maxLength={ GetConfigurationValue<number>('wired.action.chat.max.length', 100) } type="text" value={ message } onChange={ event => setMessage(event.target.value) } /> <textarea
className="form-control form-control-sm nitro-wired__resizable-textarea"
maxLength={ maxMessageLength }
rows={ 4 }
value={ message }
onChange={ event => setMessage(clampShowMessage(event.target.value)) } />
<WiredTextCounter maxLength={ maxMessageLength } value={ message } />
<WiredTextFormattingHelp />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.show_message.visibility_selection.title') }</Text> <Text bold>{ LocalizeText('wiredfurni.params.show_message.visibility_selection.title') }</Text>
@@ -0,0 +1,58 @@
import { FC, useState } from 'react';
import { Text } from '../../../../common';
export const WIRED_TEXT_MESSAGE_MAX_LENGTH = 512;
export const getWiredTextLineCount = (value: string) =>
{
if(!value.length) return 0;
return value.replace(/\r/g, '').split('\n').length;
};
interface WiredTextCounterProps
{
value: string;
maxLength?: number;
}
export const WiredTextCounter: FC<WiredTextCounterProps> = props =>
{
const { value = '', maxLength = WIRED_TEXT_MESSAGE_MAX_LENGTH } = props;
return <Text small>{ `${ getWiredTextLineCount(value) } righe - ${ value.length }/${ maxLength } caratteri` }</Text>;
};
export const WiredTextFormattingHelp: FC<{}> = () =>
{
const [ isOpen, setIsOpen ] = useState(false);
return (
<div className="flex flex-col gap-1 rounded bg-black/5 p-1 text-[11px] leading-[14px]">
<button className="w-full text-left" type="button" onClick={ () => setIsOpen(value => !value) }>
<Text small bold>{ isOpen ? 'Hide format examples' : 'Show format examples' }</Text>
</button>
{ isOpen &&
<>
<Text small>
<span className="font-bold">[b]Example Bold[/b]</span>
<span> - </span>
<span className="italic">[i]Example Italic[/i]</span>
<span> - </span>
<span className="underline">[u]Example Underline[/u]</span>
</Text>
<Text small>
<span style={ { color: '#008000' } }>[green]Example Green[/green]</span>
<span> - </span>
<span style={ { color: '#008b8b' } }>[cyan]Example Cyan[/cyan]</span>
<span> - </span>
<span style={ { color: '#d60000' } }>[red]Example Red[/red]</span>
<span> - </span>
<span style={ { color: '#005dff' } }>[blue]Example Blue[/blue]</span>
<span> - </span>
<span style={ { color: '#7d31b8' } }>[purple]Example Purple[/purple]</span>
</Text>
</> }
</div>
);
};