mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
🆙 Init V3
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { FC, MouseEvent, useEffect, useState } from 'react';
|
||||
import { ArrowContainer, Popover } from 'react-tiny-popover';
|
||||
import { Flex, Grid, NitroCardContentView } from '../../../../common';
|
||||
|
||||
interface ChatInputStyleSelectorViewProps
|
||||
{
|
||||
chatStyleId: number;
|
||||
chatStyleIds: number[];
|
||||
selectChatStyleId: (styleId: number) => void;
|
||||
}
|
||||
|
||||
export const ChatInputStyleSelectorView: FC<ChatInputStyleSelectorViewProps> = props =>
|
||||
{
|
||||
const { chatStyleId = 0, chatStyleIds = null, selectChatStyleId = null } = props;
|
||||
const [ target, setTarget ] = useState<(EventTarget & HTMLElement)>(null);
|
||||
const [ selectorVisible, setSelectorVisible ] = useState(false);
|
||||
|
||||
const selectStyle = (styleId: number) =>
|
||||
{
|
||||
selectChatStyleId(styleId);
|
||||
setSelectorVisible(false);
|
||||
};
|
||||
|
||||
const toggleSelector = (event: MouseEvent<HTMLElement>) =>
|
||||
{
|
||||
let visible = false;
|
||||
|
||||
setSelectorVisible(prevValue =>
|
||||
{
|
||||
visible = !prevValue;
|
||||
|
||||
return visible;
|
||||
});
|
||||
|
||||
if(visible) setTarget((event.target as (EventTarget & HTMLElement)));
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(selectorVisible) return;
|
||||
|
||||
setTarget(null);
|
||||
}, [ selectorVisible ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<Popover
|
||||
containerClassName="max-w-[276px] not-italic font-normal leading-normal text-left no-underline [text-shadow:none] normal-case tracking-[normal] [word-break:normal] [word-spacing:normal] whitespace-normal text-[.7875rem] [word-wrap:break-word] bg-[#dfdfdf] bg-clip-padding border-[1px] border-[solid] border-[#283F5D] rounded-[.25rem] [box-shadow:0_2px_#00000073] z-[1070]"
|
||||
content={ ({ position, childRect, popoverRect }) => (
|
||||
<ArrowContainer // if you'd like an arrow, you can import the ArrowContainer!
|
||||
arrowColor={ 'black' }
|
||||
arrowSize={ 7 }
|
||||
arrowStyle={ { bottom: 'calc(-.5rem - 1px)' } }
|
||||
childRect={ childRect }
|
||||
popoverRect={ popoverRect }
|
||||
position={ position }
|
||||
>
|
||||
<NitroCardContentView className="bg-transparent !max-h-[200px]" overflow="hidden">
|
||||
<Grid columnCount={ 3 } overflow="auto">
|
||||
{ chatStyleIds && (chatStyleIds.length > 0) && chatStyleIds.map((styleId) =>
|
||||
{
|
||||
return (
|
||||
<Flex key={ styleId } center pointer className="h-[30px]" onClick={ event => selectStyle(styleId) }>
|
||||
<div key={ styleId } className="bubble-container relative w-[50px]">
|
||||
<div className={ `relative max-w-[350px] min-h-[26px] text-[14px] chat-bubble bubble-${ styleId }` }> </div>
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
}) }
|
||||
</Grid>
|
||||
</NitroCardContentView>
|
||||
|
||||
</ArrowContainer>
|
||||
) }
|
||||
isOpen={ selectorVisible }
|
||||
positions={ [ 'top' ] }
|
||||
>
|
||||
<div className="cursor-pointer nitro-icon chatstyles-icon" onClick={ toggleSelector } />
|
||||
|
||||
</Popover>
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,248 @@
|
||||
import { GetSessionDataManager, HabboClubLevelEnum, RoomControllerLevel } from '@nitrots/nitro-renderer';
|
||||
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 { ChatInputStyleSelectorView } from './ChatInputStyleSelectorView';
|
||||
|
||||
export const ChatInputView: FC<{}> = props =>
|
||||
{
|
||||
const [ chatValue, setChatValue ] = useState<string>('');
|
||||
const { chatStyleId = 0, updateChatStyleId = null } = useSessionInfo();
|
||||
const { selectedUsername = '', floodBlocked = false, floodBlockedSeconds = 0, setIsTyping = null, setIsIdle = null, sendChat = null } = useChatInputWidget();
|
||||
const { roomSession = null } = useRoom();
|
||||
const inputRef = useRef<HTMLInputElement>();
|
||||
|
||||
const chatModeIdWhisper = useMemo(() => LocalizeText('widgets.chatinput.mode.whisper'), []);
|
||||
const chatModeIdShout = useMemo(() => LocalizeText('widgets.chatinput.mode.shout'), []);
|
||||
const chatModeIdSpeak = useMemo(() => LocalizeText('widgets.chatinput.mode.speak'), []);
|
||||
const maxChatLength = useMemo(() => GetConfigurationValue<number>('chat.input.maxlength', 100), []);
|
||||
|
||||
const anotherInputHasFocus = useCallback(() =>
|
||||
{
|
||||
const activeElement = document.activeElement;
|
||||
|
||||
if(!activeElement) return false;
|
||||
|
||||
if(inputRef && (inputRef.current === activeElement)) return false;
|
||||
|
||||
if(!(activeElement instanceof HTMLInputElement) && !(activeElement instanceof HTMLTextAreaElement)) return false;
|
||||
|
||||
return true;
|
||||
}, [ inputRef ]);
|
||||
|
||||
const setInputFocus = useCallback(() =>
|
||||
{
|
||||
inputRef.current.focus();
|
||||
|
||||
inputRef.current.setSelectionRange((inputRef.current.value.length * 2), (inputRef.current.value.length * 2));
|
||||
}, [ inputRef ]);
|
||||
|
||||
const checkSpecialKeywordForInput = useCallback(() =>
|
||||
{
|
||||
setChatValue(prevValue =>
|
||||
{
|
||||
if((prevValue !== chatModeIdWhisper) || !selectedUsername.length) return prevValue;
|
||||
|
||||
return (`${ prevValue } ${ selectedUsername }`);
|
||||
});
|
||||
}, [ selectedUsername, chatModeIdWhisper ]);
|
||||
|
||||
const sendChatValue = useCallback((value: string, shiftKey: boolean = false) =>
|
||||
{
|
||||
if(!value || (value === '')) return;
|
||||
|
||||
let chatType = (shiftKey ? ChatMessageTypeEnum.CHAT_SHOUT : ChatMessageTypeEnum.CHAT_DEFAULT);
|
||||
let text = value;
|
||||
|
||||
const parts = text.split(' ');
|
||||
|
||||
let recipientName = '';
|
||||
let append = '';
|
||||
|
||||
switch(parts[0])
|
||||
{
|
||||
case chatModeIdWhisper:
|
||||
chatType = ChatMessageTypeEnum.CHAT_WHISPER;
|
||||
recipientName = parts[1];
|
||||
append = (chatModeIdWhisper + ' ' + recipientName + ' ');
|
||||
|
||||
parts.shift();
|
||||
parts.shift();
|
||||
break;
|
||||
case chatModeIdShout:
|
||||
chatType = ChatMessageTypeEnum.CHAT_SHOUT;
|
||||
|
||||
parts.shift();
|
||||
break;
|
||||
case chatModeIdSpeak:
|
||||
chatType = ChatMessageTypeEnum.CHAT_DEFAULT;
|
||||
|
||||
parts.shift();
|
||||
break;
|
||||
}
|
||||
|
||||
text = parts.join(' ');
|
||||
|
||||
setIsTyping(false);
|
||||
setIsIdle(false);
|
||||
|
||||
if(text.length <= maxChatLength)
|
||||
{
|
||||
if(/%CC%/g.test(encodeURIComponent(text)))
|
||||
{
|
||||
setChatValue('');
|
||||
}
|
||||
else
|
||||
{
|
||||
setChatValue('');
|
||||
sendChat(text, chatType, recipientName, chatStyleId);
|
||||
}
|
||||
}
|
||||
|
||||
setChatValue(append);
|
||||
}, [ chatModeIdWhisper, chatModeIdShout, chatModeIdSpeak, maxChatLength, chatStyleId, setIsTyping, setIsIdle, sendChat ]);
|
||||
|
||||
const updateChatInput = useCallback((value: string) =>
|
||||
{
|
||||
if(!value || !value.length)
|
||||
{
|
||||
setIsTyping(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
setIsTyping(true);
|
||||
setIsIdle(true);
|
||||
}
|
||||
|
||||
setChatValue(value);
|
||||
}, [ setIsTyping, setIsIdle ]);
|
||||
|
||||
const onKeyDownEvent = useCallback((event: KeyboardEvent) =>
|
||||
{
|
||||
if(floodBlocked || !inputRef.current || anotherInputHasFocus()) return;
|
||||
|
||||
if(document.activeElement !== inputRef.current) setInputFocus();
|
||||
|
||||
const value = (event.target as HTMLInputElement).value;
|
||||
|
||||
switch(event.key)
|
||||
{
|
||||
case ' ':
|
||||
case 'Space':
|
||||
checkSpecialKeywordForInput();
|
||||
return;
|
||||
case 'NumpadEnter':
|
||||
case 'Enter':
|
||||
sendChatValue(value, event.shiftKey);
|
||||
return;
|
||||
case 'Backspace':
|
||||
if(value)
|
||||
{
|
||||
const parts = value.split(' ');
|
||||
|
||||
if((parts[0] === chatModeIdWhisper) && (parts.length === 3) && (parts[2] === ''))
|
||||
{
|
||||
setChatValue('');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
}, [ floodBlocked, inputRef, chatModeIdWhisper, anotherInputHasFocus, setInputFocus, checkSpecialKeywordForInput, sendChatValue ]);
|
||||
|
||||
useUiEvent<RoomWidgetUpdateChatInputContentEvent>(RoomWidgetUpdateChatInputContentEvent.CHAT_INPUT_CONTENT, event =>
|
||||
{
|
||||
switch(event.chatMode)
|
||||
{
|
||||
case RoomWidgetUpdateChatInputContentEvent.WHISPER: {
|
||||
setChatValue(`${ chatModeIdWhisper } ${ event.userName } `);
|
||||
return;
|
||||
}
|
||||
case RoomWidgetUpdateChatInputContentEvent.SHOUT:
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
const chatStyleIds = useMemo(() =>
|
||||
{
|
||||
let styleIds: number[] = [];
|
||||
|
||||
const styles = GetConfigurationValue<{ styleId: number, minRank: number, isSystemStyle: boolean, isHcOnly: boolean, isAmbassadorOnly: boolean }[]>('chat.styles');
|
||||
|
||||
for(const style of styles)
|
||||
{
|
||||
if(!style) continue;
|
||||
|
||||
if(style.minRank > 0)
|
||||
{
|
||||
if(GetSessionDataManager().hasSecurity(style.minRank)) styleIds.push(style.styleId);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if(style.isSystemStyle)
|
||||
{
|
||||
if(GetSessionDataManager().hasSecurity(RoomControllerLevel.MODERATOR))
|
||||
{
|
||||
styleIds.push(style.styleId);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if(GetConfigurationValue<number[]>('chat.styles.disabled').indexOf(style.styleId) >= 0) continue;
|
||||
|
||||
if(style.isHcOnly && (GetClubMemberLevel() >= HabboClubLevelEnum.CLUB))
|
||||
{
|
||||
styleIds.push(style.styleId);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if(style.isAmbassadorOnly && GetSessionDataManager().isAmbassador)
|
||||
{
|
||||
styleIds.push(style.styleId);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!style.isHcOnly && !style.isAmbassadorOnly) styleIds.push(style.styleId);
|
||||
}
|
||||
|
||||
return styleIds;
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
document.body.addEventListener('keydown', onKeyDownEvent);
|
||||
|
||||
return () =>
|
||||
{
|
||||
document.body.removeEventListener('keydown', onKeyDownEvent);
|
||||
};
|
||||
}, [ onKeyDownEvent ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!inputRef.current) return;
|
||||
|
||||
inputRef.current.parentElement.dataset.value = chatValue;
|
||||
}, [ chatValue ]);
|
||||
|
||||
if(!roomSession || roomSession.isSpectator) return null;
|
||||
|
||||
return (
|
||||
createPortal(
|
||||
<div className="nitro-chat-input-container flex justify-center items-center relative h-10 border-2 border-black bg-gray-200 pr-2.5 w-full overflow-hidden rounded-lg">
|
||||
<div className="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() } /> }
|
||||
{ floodBlocked &&
|
||||
<Text variant="danger">{ LocalizeText('chat.input.alert.flood', [ 'time' ], [ floodBlockedSeconds.toString() ]) } </Text> }
|
||||
</div>
|
||||
<ChatInputStyleSelectorView chatStyleId={ chatStyleId } chatStyleIds={ chatStyleIds } selectChatStyleId={ updateChatStyleId } />
|
||||
</div>, document.getElementById('toolbar-chat-input-container'))
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user