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:
Lorenzune
2026-03-21 14:47:52 +01:00
60 changed files with 9794 additions and 354 deletions
@@ -220,8 +220,8 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
canUse = true;
isCrackable = true;
crackableHits = stuffData.hits;
crackableTarget = stuffData.target;
crackableHits = stuffData?.hits ?? 0;
crackableTarget = stuffData?.target ?? 0;
}
else if(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX)
@@ -527,7 +527,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
{ isCrackable &&
<>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
<Text small wrap variant="white">{ LocalizeText('infostand.crackable_furni.hits_remaining', [ 'hits', 'target' ], [ crackableHits.toString(), crackableTarget.toString() ]) }</Text>
<Text small wrap variant="white">{ LocalizeText('infostand.crackable_furni.hits_remaining', [ 'hits', 'target' ], [ (crackableHits ?? 0).toString(), (crackableTarget ?? 0).toString() ]) }</Text>
</> }
{ avatarInfo.groupId > 0 &&
<>
@@ -552,7 +552,21 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
{ godMode &&
<>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
{ canSeeFurniId && <Text small wrap variant="white">ID: { avatarInfo.id }</Text> }
{ canSeeFurniId &&
<div className="flex items-center gap-3">
<div className="flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-3 h-3 text-[#7ec8e3]">
<path fillRule="evenodd" d="M4.93 1.31a41.401 41.401 0 0 1 10.14 0C16.194 1.45 17 2.414 17 3.517V18.25a.75.75 0 0 1-1.075.676l-2.8-1.344-2.8 1.344a.75.75 0 0 1-.65 0l-2.8-1.344-2.8 1.344A.75.75 0 0 1 3 18.25V3.517c0-1.103.806-2.068 1.93-2.207Z" clipRule="evenodd" />
</svg>
<Text small wrap variant="white">ID: { avatarInfo.id }</Text>
</div>
<div className="flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-3 h-3 text-[#7ec8e3]">
<path d="M5.127 3.502 5.25 3.5h9.5c.041 0 .082 0 .123.002A2.251 2.251 0 0 0 12.75 2h-5.5a2.25 2.25 0 0 0-2.123 1.502ZM1 10.25A2.25 2.25 0 0 1 3.25 8h13.5A2.25 2.25 0 0 1 19 10.25v5.5A2.25 2.25 0 0 1 16.75 18H3.25A2.25 2.25 0 0 1 1 15.75v-5.5ZM3.25 6.5c-.04 0-.082 0-.123.002A2.25 2.25 0 0 1 5.25 5h9.5c.98 0 1.814.627 2.123 1.502a3.819 3.819 0 0 0-.123-.002H3.25Z" />
</svg>
<Text small wrap variant="white">Sprite: { (() => { const ro = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR); return ro?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID) ?? '?'; })() }</Text>
</div>
</div> }
{ (!avatarInfo.isWallItem && canMove) &&
<>
<button
@@ -560,6 +574,19 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
onClick={ () => setDropdownOpen(!dropdownOpen) }>
{ dropdownOpen ? `${LocalizeText('widget.furni.present.close')} Buildtools` : `${LocalizeText('navigator.roomsettings.doormode.open')} Buildtools` }
</button>
<button
className="w-full text-white text-xs bg-[#1e7295] hover:bg-[#1a617f] border border-[#ffffff33] rounded px-2 py-1 cursor-pointer transition-colors"
onClick={ () =>
{
const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR);
const typeId = roomObject?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID);
CreateLinkEvent('furni-editor/show');
if(typeId) window.dispatchEvent(new CustomEvent('furni-editor:open', { detail: { spriteId: typeId } }));
} }>
Edit Furni
</button>
{ dropdownOpen &&
<div className="flex gap-[4px] w-full">
{ /* Left panel: position + rotation */ }
@@ -0,0 +1,41 @@
import { FC, useEffect, useRef } from 'react';
import { CommandDefinition } from '../../../../api';
interface ChatInputCommandSelectorViewProps
{
commands: CommandDefinition[];
selectedIndex: number;
onSelect: (command: CommandDefinition) => void;
onHover: (index: number) => void;
}
export const ChatInputCommandSelectorView: FC<ChatInputCommandSelectorViewProps> = props =>
{
const { commands = [], selectedIndex = 0, onSelect = null, onHover = null } = props;
const listRef = useRef<HTMLDivElement>(null);
useEffect(() =>
{
if(!listRef.current) return;
const selected = listRef.current.children[selectedIndex] as HTMLElement;
if(selected) selected.scrollIntoView({ block: 'nearest' });
}, [ selectedIndex ]);
return (
<div ref={ listRef } className="absolute bottom-full left-0 w-full bg-[#e8e8e8] border-2 border-black border-b-0 rounded-t-lg max-h-[240px] overflow-y-auto z-[1070]">
{ commands.map((cmd, index) => (
<div
key={ cmd.key }
className={ `px-3 py-1.5 cursor-pointer text-sm flex items-center gap-2 ${ index === selectedIndex ? 'bg-[#283F5D] text-white' : 'hover:bg-gray-300' }` }
onClick={ () => onSelect(cmd) }
onMouseEnter={ () => onHover(index) }
>
<span className="font-bold">:{ cmd.key }</span>
<span className={ `text-xs ${ index === selectedIndex ? 'text-gray-300' : 'text-gray-500' }` }>{ cmd.description }</span>
</div>
)) }
</div>
);
};
@@ -3,7 +3,8 @@ import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { ChatMessageTypeEnum, GetClubMemberLevel, GetConfigurationValue, LocalizeText, RoomWidgetUpdateChatInputContentEvent } from '../../../../api';
import { Text } from '../../../../common';
import { useChatInputWidget, useRoom, useSessionInfo, useUiEvent } from '../../../../hooks';
import { useChatCommandSelector, useChatInputWidget, useRoom, useSessionInfo, useUiEvent } from '../../../../hooks';
import { ChatInputCommandSelectorView } from './ChatInputCommandSelectorView';
import { ChatInputEmojiSelectorView } from './ChatInputEmojiSelectorView';
import { ChatInputStyleSelectorView } from './ChatInputStyleSelectorView';
@@ -14,6 +15,7 @@ export const ChatInputView: FC<{}> = props =>
const { selectedUsername = '', floodBlocked = false, floodBlockedSeconds = 0, setIsTyping = null, setIsIdle = null, sendChat = null } = useChatInputWidget();
const { roomSession = null } = useRoom();
const inputRef = useRef<HTMLInputElement>();
const { isVisible: commandSelectorVisible, filteredCommands, selectedIndex, setSelectedIndex, moveUp, moveDown, selectCurrent, close: closeCommandSelector } = useChatCommandSelector(chatValue);
const chatModeIdWhisper = useMemo(() => LocalizeText('widgets.chatinput.mode.whisper'), []);
const chatModeIdShout = useMemo(() => LocalizeText('widgets.chatinput.mode.shout'), []);
@@ -133,6 +135,40 @@ export const ChatInputView: FC<{}> = props =>
if(document.activeElement !== inputRef.current) setInputFocus();
if(commandSelectorVisible)
{
switch(event.key)
{
case 'ArrowUp':
event.preventDefault();
moveUp();
return;
case 'ArrowDown':
event.preventDefault();
moveDown();
return;
case 'Tab':
event.preventDefault();
// fall through
case 'NumpadEnter':
case 'Enter': {
const selected = selectCurrent();
if(selected)
{
event.preventDefault();
setChatValue(':' + selected.key + ' ');
return;
}
break;
}
case 'Escape':
event.preventDefault();
closeCommandSelector();
return;
}
}
const value = (event.target as HTMLInputElement).value;
switch(event.key)
@@ -158,7 +194,7 @@ export const ChatInputView: FC<{}> = props =>
return;
}
}, [ floodBlocked, inputRef, chatModeIdWhisper, anotherInputHasFocus, setInputFocus, checkSpecialKeywordForInput, sendChatValue ]);
}, [ floodBlocked, inputRef, chatModeIdWhisper, anotherInputHasFocus, setInputFocus, checkSpecialKeywordForInput, sendChatValue, commandSelectorVisible, moveUp, moveDown, selectCurrent, closeCommandSelector ]);
useUiEvent<RoomWidgetUpdateChatInputContentEvent>(RoomWidgetUpdateChatInputContentEvent.CHAT_INPUT_CONTENT, event =>
{
@@ -243,7 +279,14 @@ export const ChatInputView: FC<{}> = props =>
return (
createPortal(
<div className="nitro-chat-input-container flex justify-between items-center relative h-10 border-2 border-black bg-gray-200 pr-2.5 w-full overflow-hidden rounded-lg">
<div className="nitro-chat-input-container flex justify-between items-center relative h-10 border-2 border-black bg-gray-200 pr-2.5 w-full overflow-visible rounded-lg">
{ commandSelectorVisible &&
<ChatInputCommandSelectorView
commands={ filteredCommands }
selectedIndex={ selectedIndex }
onSelect={ (cmd) => { setChatValue(':' + cmd.key + ' '); inputRef.current?.focus(); } }
onHover={ setSelectedIndex }
/> }
<div className="flex-1 items-center input-sizer">
{ !floodBlocked &&
<input ref={ inputRef } className="[font-size:inherit] placeholder-[#6c757d] bg-transparent border-none focus:border-current focus:shadow-none focus:ring-0 " maxLength={ maxChatLength } placeholder={ LocalizeText('widgets.chatinput.default') } type="text" value={ chatValue } onChange={ event => updateChatInput(event.target.value) } onMouseDown={ event => setInputFocus() } /> }
@@ -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>
@@ -5,6 +5,7 @@ import { FC, useEffect, useState } from 'react';
import { GetConfigurationValue, LocalizeText, SendMessageComposer, SetLocalStorage, TryVisitRoom } from '../../../../api';
import { Text } from '../../../../common';
import { useMessageEvent, useNavigator, useRoom } from '../../../../hooks';
import { getRegisteredPlugins, INitroPlugin, subscribePlugins } from '../../../plugins/NitroPluginApi';
export const RoomToolsWidgetView: FC<{}> = props => {
const [areBubblesMuted, setAreBubblesMuted] = useState(false);
@@ -15,12 +16,20 @@ export const RoomToolsWidgetView: FC<{}> = props => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [isOpenHistory, setIsOpenHistory] = useState<boolean>(false);
const [roomHistory, setRoomHistory] = useState<{ roomId: number, roomName: string }[]>([]);
const [plugins, setPlugins] = useState<INitroPlugin[]>([]);
const { navigatorData = null } = useNavigator();
const { roomSession = null } = useRoom();
// Subscribe to external plugin changes
useEffect(() =>
{
setPlugins(getRegisteredPlugins());
return subscribePlugins(() => setPlugins(getRegisteredPlugins()));
}, []);
const handleToolClick = (action: string, value?: string) => {
if (!roomSession) return;
switch (action) {
case 'settings':
CreateLinkEvent('navigator/toggle-room-info');
@@ -114,12 +123,20 @@ export const RoomToolsWidgetView: FC<{}> = props => {
<div className={classNames('cursor-pointer', 'nitro-icon', (!isZoomedIn && 'icon-zoom-less'), (isZoomedIn && 'icon-zoom-more'))} title={LocalizeText('room.zoom.button.text')} onClick={() => handleToolClick('zoom')} />
<div className="cursor-pointer nitro-icon icon-chat-history" title={LocalizeText('room.chathistory.button.text')} onClick={() => handleToolClick('chat_history')} />
<div className={classNames('cursor-pointer', 'nitro-icon', (areBubblesMuted ? 'icon-chat-disablebubble' : 'icon-chat-enablebubble'))} title={areBubblesMuted ? LocalizeText('room.unmute.button.text') : LocalizeText('room.mute.button.text')} onClick={() => handleToolClick('hiddenbubbles')} />
{navigatorData.canRate && (
<div className="cursor-pointer nitro-icon icon-like-room" title={LocalizeText('room.like.button.text')} onClick={() => handleToolClick('like_room')} />
)}
<div className="cursor-pointer nitro-icon icon-room-link" title={LocalizeText('navigator.embed.caption')} onClick={() => handleToolClick('toggle_room_link')} />
<div className="cursor-pointer nitro-icon icon-room-history-enabled" title={LocalizeText('room.history.button.tooltip')} onClick={() => handleToolClick('room_history')} />
{plugins.map(plugin => (
<div
key={plugin.name}
className={`cursor-pointer nitro-icon ${plugin.icon || 'icon-cog'}`}
title={plugin.label}
onClick={() => plugin.onOpen()}
/>
))}
</div>
<div className="flex flex-col justify-center">
<AnimatePresence>
@@ -159,4 +176,4 @@ export const RoomToolsWidgetView: FC<{}> = props => {
</div>
</div>
);
};
};