Added: ChatColors / Make stuff LocalCache in browser

This commit is contained in:
duckietm 2024-04-03 11:44:56 +02:00
parent c00f4329d3
commit e1bcafbb28
15 changed files with 455 additions and 97 deletions

View File

@ -22,10 +22,13 @@ export class ChatBubbleMessage
public type: number = 0, public type: number = 0,
public styleId: number = 0, public styleId: number = 0,
public imageUrl: string = null, public imageUrl: string = null,
public color: string = null public color: string = null,
public chatColours: string = ""
) )
{ {
this.id = ++ChatBubbleMessage.BUBBLE_COUNTER; this.id = ++ChatBubbleMessage.BUBBLE_COUNTER;
this.color = color;
this.chatColours = chatColours;
} }
public get top(): number public get top(): number

View File

@ -1,4 +1,5 @@
const allowedColours: Map<string, string> = new Map(); import { GetConfiguration } from '@nitrots/nitro-renderer';
export const allowedColours: Map<string, string> = new Map();
allowedColours.set('r', 'red'); allowedColours.set('r', 'red');
allowedColours.set('b', 'blue'); allowedColours.set('b', 'blue');
@ -11,16 +12,109 @@ allowedColours.set('br', 'brown');
allowedColours.set('pr', 'purple'); allowedColours.set('pr', 'purple');
allowedColours.set('pk', 'pink'); allowedColours.set('pk', 'pink');
allowedColours.set('black', 'black');
allowedColours.set('red', 'red'); allowedColours.set('red', 'red');
allowedColours.set('blue', 'blue'); allowedColours.set('orangered', 'orangered');
allowedColours.set('green', 'green');
allowedColours.set('yellow', 'yellow');
allowedColours.set('white', 'white');
allowedColours.set('orange', 'orange'); allowedColours.set('orange', 'orange');
allowedColours.set('cyan', 'cyan'); allowedColours.set('yellow', 'yellow');
allowedColours.set('brown', 'brown'); allowedColours.set('yellowgreen', 'yellowgreen');
allowedColours.set('green', 'green');
allowedColours.set('seagreen', 'seagreen');
allowedColours.set('teal', 'teal');
allowedColours.set('blue', 'blue');
allowedColours.set('darkblue', 'darkblue');
allowedColours.set('indigo', 'indigo');
allowedColours.set('purple', 'purple'); allowedColours.set('purple', 'purple');
allowedColours.set('pink', 'pink'); allowedColours.set('violet', 'violet');
allowedColours.set('brown', 'brown');
allowedColours.set('burlywood', 'burlywood');
allowedColours.set('rosybrown', 'rosybrown');
allowedColours.set('saddlebrown', 'saddlebrown');
allowedColours.set('maroon', 'maroon');
allowedColours.set('firebrick', 'firebrick');
allowedColours.set('darkred', 'darkred');
allowedColours.set('chocolate', 'chocolate');
allowedColours.set('sienna', 'sienna');
allowedColours.set('peru', 'peru');
allowedColours.set('darkorange', 'darkorange');
allowedColours.set('orange', 'orange');
allowedColours.set('orangered', 'orangered');
allowedColours.set('tomato', 'tomato');
allowedColours.set('coral', 'coral');
allowedColours.set('darkolivegreen', 'darkolivegreen');
allowedColours.set('olive', 'olive');
allowedColours.set('olivedrab', 'olivedrab');
allowedColours.set('greenyellow', 'greenyellow');
allowedColours.set('yellowgreen', 'yellowgreen');
allowedColours.set('darkgreen', 'darkgreen');
allowedColours.set('limegreen', 'limegreen');
allowedColours.set('lime', 'lime');
allowedColours.set('lawngreen', 'lawngreen');
allowedColours.set('palegreen', 'palegreen');
allowedColours.set('springgreen', 'springgreen');
allowedColours.set('mediumseagreen', 'mediumseagreen');
allowedColours.set('mediumaquamarine', 'mediumaquamarine');
allowedColours.set('aquamarine', 'aquamarine');
allowedColours.set('turquoise', 'turquoise');
allowedColours.set('mediumturquoise', 'mediumturquoise');
allowedColours.set('darkturquoise', 'darkturquoise');
allowedColours.set('aqua', 'aqua');
allowedColours.set('cyan', 'cyan');
allowedColours.set('lightcyan', 'lightcyan');
allowedColours.set('paleturquoise', 'paleturquoise');
allowedColours.set('azure', 'azure');
allowedColours.set('lightblue', 'lightblue');
allowedColours.set('powderblue', 'powderblue');
allowedColours.set('deepskyblue', 'deepskyblue');
allowedColours.set('skyblue', 'skyblue');
allowedColours.set('lightskyblue', 'lightskyblue');
allowedColours.set('steelblue', 'steelblue');
allowedColours.set('royalblue', 'royalblue');
allowedColours.set('mediumslateblue', 'mediumslateblue');
allowedColours.set('slateblue', 'slateblue');
allowedColours.set('darkslateblue', 'darkslateblue');
allowedColours.set('mediumpurple', 'mediumpurple');
allowedColours.set('blueviolet', 'blueviolet');
allowedColours.set('darkviolet', 'darkviolet');
allowedColours.set('darkmagenta', 'darkmagenta');
allowedColours.set('mediumvioletred', 'mediumvioletred');
allowedColours.set('violetred', 'violetred');
allowedColours.set('orchid', 'orchid');
allowedColours.set('darkorchid', 'darkorchid');
allowedColours.set('mediumorchid', 'mediumorchid');
allowedColours.set('thistle', 'thistle');
allowedColours.set('plum', 'plum');
allowedColours.set('purple', 'purple');
allowedColours.set('darkgrey', 'darkgrey');
allowedColours.set('dimgray', 'dimgray');
allowedColours.set('lightgrey', 'lightgrey');
allowedColours.set('grey', 'grey');
allowedColours.set('slategrey', 'slategrey');
allowedColours.set('lightslategrey', 'lightslategrey');
allowedColours.set('whitesmoke', 'whitesmoke');
allowedColours.set('white', 'white');
allowedColours.set('snow', 'snow');
allowedColours.set('mistyrose', 'mistyrose');
allowedColours.set('seashell', 'seashell');
allowedColours.set('antiquewhite', 'antiquewhite');
allowedColours.set('linen', 'linen');
allowedColours.set('oldlace', 'oldlace');
allowedColours.set('papayawhip', 'papayawhip');
allowedColours.set('blanchedalmond', 'blanchedalmond');
allowedColours.set('moccasin', 'moccasin');
allowedColours.set('wheat', 'wheat');
allowedColours.set('navajowhite', 'navajowhite');
allowedColours.set('burlywood', 'burlywood');
allowedColours.set('tan', 'tan');
allowedColours.set('rosybrown', 'rosybrown');
allowedColours.set('sandybrown', 'sandybrown');
allowedColours.set('goldenrod', 'goldenrod');
allowedColours.set('darkgoldenrod', 'darkgoldenrod');
allowedColours.set('peru', 'peru');
allowedColours.set('chocolate', 'chocolate');
allowedColours.set('saddlebrown', 'saddlebrown');
allowedColours.set('sienna', 'sienna');
allowedColours.set('brown', 'brown');
const encodeHTML = (str: string) => const encodeHTML = (str: string) =>
{ {
@ -37,37 +131,38 @@ const encodeHTML = (str: string) =>
}); });
} }
export const RoomChatFormatter = (content: string) => export const RoomChatFormatter = (content: string) => {
{
let result = ''; let result = '';
content = encodeHTML(content); content = encodeHTML(content);
//content = (joypixels.shortnameToUnicode(content) as string) content = content.replace(/\[tag\](.*?)\[\/tag\]/g, '<span class="chat-tag"><b>$1</b></span>');
if(content.startsWith('@') && content.indexOf('@', 1) > -1) // Youtube link
{ if (!GetConfiguration<boolean>('youtube.publish.disabled', false)) {
let match = null; content = content.replace(
/(?:http:\/\/|https:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?.*v=|shorts\/)?([a-zA-Z0-9_-]{11})/g,
while((match = /@[a-zA-Z]+@/g.exec(content)) !== null) `
{ <div>
const colorTag = match[0].toString(); <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEQElEQVR4nO2X7WscRRzHt+AbfeEfICK+VVTwjWhVUnI7l8SkDY3Wu5ltfbiZXBNbq8ldiZEkVbHSIAEjCtUWNLXFaNWQElOUomKwD0bxoVLtk3I+oPhUjbvZ5x2Zmb29DUlaa3fTO9gvDOzMbx5+n535zYMkJUqUKFFNKoVIK0CEyogcKZfJCO/2y3qkaldLPn+ZDLEGELYbFeVyVgYgLnGADL5GqgUBRCaYwwC1NzUq5EruPCQnpVpRGpG87/RgGhJFLB88xGzyuvxVAOGtAOIXmU2SpGWsHKzNXcfaySh/Pe9DyeVSkNxzUQDkbO4KgIjH4kBGZLuAaa/j5ZD8LmZHJBniAd4Gtj8kynCXyBNVRuRX6WIJIDLN4gBA/CNzuq5uyyWhmRllQL69VJUAMsQDlT+NR3ynHhXLKVfkkJCcAZDoVQmQyuZuLAOkYPudNQcgSdIysZ0SWq/cf20tAkgyJDMJQHXNAN7ox8WTN6/pulRG2AAI/xZeQjLEj7ATG0BsVd0Sqs/iG2REHO64f70ACL/BbKEtVpUh+VvUqzIAJqDgewEip5lNRmQfO9xYeUNm/dUAkZ8AwidkmL+tKoI40VLJeGZbv97dWdIIVLVcRtOUVlPNrrR4urvZVlvrvSClb6Fq6iaR2HfYxur67TSl1eR9Eaiyvo1nB/ticV7vL04HDsWc9P5i8EiKRObw4GNL5bzqJzbbkQHo3R0/sE7dE99Qb2ZmXnK++HRRR6zxvbyO9fru85uFro7vIwPQcFZlnXq//EwXkvvtqUUdsQ/s53Xst8fOC0DLZdToANa1Gfyv9DxIjSd6qVv6Tjh+/Gue14sPRA+wdrURGYCaXWmHO3e+/Iw75Rz8UEx3X4Far45Qvb9AtUwz/7Z27ZwLMPUeNV9+gVqju6i2GlTW+uDj1Jl6n9ofHOD9BONkW+zoANrS7tkA7Mlx4eT+fVTvWi/WlefOAQjL+Ui0M3c8P8+mFzeIcdrSbnQALSu8CwVwjx0N6nnqP9zm/fmHaPfOBHVPnwz64OO0rPCiA2hYTi8UgMWA3vuwb/Ooesftlb9e6KTWm6Oiz+lDYpyG5TQ6gPCpGhVAc10A4GkapabpAxyOASCCJXQ2gLCcQ1MxLKEIgngOAKVzAPTuDrZtUn3zRjpLYAxBfI5t9H8BhGOguIFab/kx8PHB6LfR8kF2LgD31HG+2ywE4Bz9nFp7XhI22xK70F9neJYFsHvsK2GaHI/+ICtfJRYDsF7ZWVnEtr0gQFjMWd5u7555Nr1nU/RXifJlrpzM54aoPTFGjaGnxGB3NVL73Ul+cZvdRLjNGntNnLTbtvC8uX2Y/2m258/et8bfaW6l1sgODuR8coQaW/viuczV/HWaiT0ylsp5fWDzYSkOseeeXgg9KeEqq/KkbHL++5OyyQmelHCVFTwpC50lY/jp3licT5QoUSIpDv0L7jL5ksuHFDUAAAAASUVORK5CYII="
const colorName = colorTag.substr(1, colorTag.length - 2); alt="YouTube Icon" style="vertical-align: middle; margin-right: 5px;"><strong>Click on open video to see the YouTube video</strong>
const text = content.replace(colorTag, ''); </div><center><a href="https://youtu.be/$1" target="_blank" style="background-color: red; color: white; padding: 5px 10px; border-radius: 5px; text-decoration: none;">Open Video</a></center>
`
if(!allowedColours.has(colorName)) );
{
result = text;
}
else
{
const color = allowedColours.get(colorName);
result = '<span style="color: ' + color + '">' + text + '</span>';
}
break;
}
} }
else
{ const match = content.match(/@[a-zA-Z]+@/);
if (match) {
const colorTag = match[0].toString();
const colorName = colorTag.substr(1, colorTag.length - 2);
const text = content.replace(colorTag, '');
if (!allowedColours.has(colorName)) {
result = text;
} else {
const color = allowedColours.get(colorName);
result = `<span style="color: ${color}">${text}</span>`;
}
} else {
result = content; result = content;
} }

View File

@ -0,0 +1,66 @@
import { FC, useEffect, useRef, useState } from 'react';
import { Overlay, Popover } from 'react-bootstrap';
import { MdFormatColorText } from 'react-icons/md';
import { allowedColours } from '../../../../api';
import { AutoGrid, Base, LayoutGridItem, NitroCardContentView } from '../../../../common';
interface ChatInputColorSelectorViewProps
{
chatColour: string;
selectColour: (color: string) => void;
}
export const ChatInputColorSelectorView: FC<ChatInputColorSelectorViewProps> = props =>
{
const { chatColour = 'black', selectColour = null } = props;
const [ selectorVisible, setSelectorVisible ] = useState(false);
const [ colours, setColours ] = useState<Map<string, string>>(null);
const iconRef = useRef<HTMLDivElement>(null);
useEffect(() =>
{
const excludedColors = new Set(["r", "b", "g", "y", "w", "o", "c", "br", "pr", "pk"]);
const uniqueColours = new Map<string, string>();
allowedColours.forEach((value, key) => {
if (!excludedColors.has(key) && !Array.from(uniqueColours.values()).includes(value)) {
uniqueColours.set(key, value);
}
});
setColours(uniqueColours);
}, []);
const selectColor = (color: string) =>
{
selectColour(color);
setSelectorVisible(false);
}
const toggleSelector = () =>
{
setSelectorVisible(prevValue => !prevValue);
}
return (
<>
<Base pointer onClick={ () => toggleSelector() } innerRef={ iconRef } style={ { color: (colours && colours.get(chatColour)) ?? 'black' } }>
<MdFormatColorText />
</Base>
<Overlay show={ selectorVisible } target={ iconRef } placement="top">
<Popover className="nitro-chat-style-selector-container">
<NitroCardContentView overflow="hidden" className="bg-transparent colour-container image-rendering-pixelated">
<AutoGrid gap={ 1 } columnCount={ 6 } columnMinWidth={ 20 } columnMinHeight={ 20 }>
{ colours && (colours.size > 0) && Array.from(colours).map(([ color, hex ]) =>
{
return (
<LayoutGridItem itemHighlight itemColor={ hex } itemActive={ chatColour === color } className="clear-bg" onClick={ event => selectColor(color) } key={ color }></LayoutGridItem>
);
}) }
</AutoGrid>
</NitroCardContentView>
</Popover>
</Overlay>
</>
);
}

View File

@ -0,0 +1,69 @@
import { FC, MouseEvent, useEffect, useRef, useState } from 'react';
import { Overlay, Popover } from 'react-bootstrap';
import { Base, Flex, Grid, NitroCardContentView } from '../../../../common';
import data from '@emoji-mart/data'
import Picker from '@emoji-mart/react'
interface ChatInputEmojiSelectorViewProps
{
addChatEmoji: (emoji: string) => void;
}
export const ChatInputEmojiSelectorView: FC<ChatInputEmojiSelectorViewProps> = props =>
{
const { addChatEmoji = null } = props;
const [ selectorVisible, setSelectorVisible ] = useState(false);
const [ target, setTarget ] = useState<(EventTarget & HTMLElement)>(null);
const iconRef = useRef<HTMLDivElement>(null);
const componentRef = useRef<HTMLDivElement>(null);
const handleEmojiSelect = (emoji: any) => {
addChatEmoji(emoji.native);
setSelectorVisible(false);
}
const addEmojiToChat = (emoji: string) => {
setChatValue(chatValue + emoji);
setIsTyping(true);
};
const handleClickOutside = (event: MouseEvent) => {
const className = 'emoji-icon';
if (componentRef.current && !componentRef.current.contains(event.target as Node) && !(event.target as Element).classList.contains(className)) {
setSelectorVisible(false);
document.removeEventListener('mousedown', handleClickOutside as any);
setTarget(null);
}
};
const selectEmoji = (emoji: string) =>
{
addChatEmoji(emoji);
}
const toggleSelector = (event: MouseEvent<HTMLElement>) =>
{
setSelectorVisible(prevValue => !prevValue);
}
useEffect(() =>
{
if(selectorVisible) {
document.addEventListener('mousedown', handleClickOutside as any);
}
else {
setTarget(null);
}
}, [ componentRef, selectorVisible ]);
return (
<>
<Base pointer onClick={toggleSelector} innerRef={iconRef}>🙂</Base>
<Overlay show={selectorVisible} target={iconRef} placement="top-end">
<Popover>
<Picker data={data} onEmojiSelect={handleEmojiSelect} />
</Popover>
</Overlay>
</>
);
}

View File

@ -46,9 +46,9 @@ export const ChatInputStyleSelectorView: FC<ChatInputStyleSelectorViewProps> = p
<> <>
<Base pointer className="icon chatstyles-icon" onClick={ toggleSelector } /> <Base pointer className="icon chatstyles-icon" onClick={ toggleSelector } />
<Overlay show={ selectorVisible } target={ target } placement="top"> <Overlay show={ selectorVisible } target={ target } placement="top">
<Popover className="nitro-chat-style-selector-container image-rendering-pixelated"> <Popover className="nitro-chat-style-selector-container">
<NitroCardContentView overflow="hidden" className="bg-transparent"> <NitroCardContentView overflow="hidden" className="bg-transparent bubble-window image-rendering-pixelated">
<Grid columnCount={ 3 } overflow="auto"> <Grid gap={ 1 } columnCount={ 3 } overflow="auto">
{ chatStyleIds && (chatStyleIds.length > 0) && chatStyleIds.map((styleId) => { chatStyleIds && (chatStyleIds.length > 0) && chatStyleIds.map((styleId) =>
{ {
return ( return (

View File

@ -1,29 +1,30 @@
.nitro-chat-input-container { .nitro-chat-input-container {
display: flex; display: flex;
justify-content: center; justify-content: space-between;
align-items: center; align-items: center;
position: relative; position: relative;
height: 40px; height: 40px;
border-radius: 8px; border-radius: 0.5rem;
border: 2px solid rgb(0, 0, 0); border: 2px solid #000;
background: #EDEDED; box-shadow: 0 0 0 1pt #fff!important;
background: #ededed;
padding-right: 10px; padding-right: 10px;
width: 100%;
overflow: hidden; overflow: hidden;
width: -webkit-fill-available;
@include media-breakpoint-down(sm) { @include media-breakpoint-down(xxl) {
display: flex;
position: absolute; position: absolute;
bottom: 70px; bottom: 70px;
left: calc(100% / 3); left: 50%;
width: 200px; transform: translateX(-50%);
width: auto;
} }
&:before { &:before {
content: ''; content: '';
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 5px; height: 2px;
top: 1px; top: 1px;
left: 0; left: 0;
right: 0; right: 0;
@ -51,18 +52,45 @@
border: none; border: none;
outline: none; outline: none;
} }
&::after { &::after {
content: attr(data-value) ' '; content: attr(data-value) ' ';
visibility: hidden; visibility: hidden;
white-space: pre-wrap; white-space: pre-wrap;
} }
}
.colour-container {
visibility: visible;
width: 100%;
} }
.bubble-container { .bubble-container {
visibility: visible; visibility: visible;
width: 75%; width: 75%;
} }
.nitro-chat-style-box {
height: 100%;
z-index: 1;
display: flex;
align-items: center;
padding: 0 0.75em;
margin-right: 2px;
width: 34px;
}
}
.nitro-chat-input-container .input-sizer input{
width: auto;
min-width: 1em;
grid-area: 1 / 2;
margin: 0;
resize: none;
background: none;
appearance: none;
border: none;
outline: none;
} }
.nitro-chat-style-selector-container { .nitro-chat-style-selector-container {
@ -73,6 +101,34 @@
max-height: $chat-input-style-selector-widget-height !important; max-height: $chat-input-style-selector-widget-height !important;
} }
.emoji {
position: relative;
z-index: 1;
transition: transform 0.3s;
}
.emoji::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: lightblue;
border-radius: 60%;
z-index: -1;
opacity: 0;
transition: opacity 0.3s;
}
.emoji:hover::before {
opacity: 1;
}
.emoji:hover {
transform: scale(1.5);
}
.bubble-parent-container { .bubble-parent-container {
height: 30px; height: 30px;
@ -82,3 +138,20 @@
} }
} }
} }
.info-habbopages {
cursor: pointer;
background-image: url("@/assets/images/boxes/card/questionmark.png");
width: 19px;
height: 20px;
right: -2px;
position: relative;
&:hover {
background-image: url("@/assets/images/boxes/card/questionmark_hover.png");
&:active {
background-image: url("@/assets/images/boxes/card/questionmark_click.png");
}
}
}

View File

@ -1,19 +1,23 @@
import { GetSessionDataManager, HabboClubLevelEnum, RoomControllerLevel } from '@nitrots/nitro-renderer'; import { CreateLinkEvent, GetSessionDataManager, HabboClubLevelEnum, RoomControllerLevel } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ChatMessageTypeEnum, GetClubMemberLevel, GetConfigurationValue, LocalizeText, RoomWidgetUpdateChatInputContentEvent } from '../../../../api'; import { ChatMessageTypeEnum, GetConfigurationValue, GetClubMemberLevel, LocalizeText, RoomWidgetUpdateChatInputContentEvent } from '../../../../api';
import { Text } from '../../../../common'; import { Base, Flex, Text } from '../../../../common';
import { useChatInputWidget, useRoom, useSessionInfo, useUiEvent } from '../../../../hooks'; import { useChatInputWidget, useRoom, useSessionInfo, useUiEvent } from '../../../../hooks';
import { ChatInputStyleSelectorView } from './ChatInputStyleSelectorView'; import { ChatInputStyleSelectorView } from './ChatInputStyleSelectorView';
import { ChatInputEmojiSelectorView } from './ChatInputEmojiSelectorView';
import { ChatInputColorSelectorView } from './ChatInputColorSelectorView';
export const ChatInputView: FC<{}> = props => export const ChatInputView: FC<{}> = props =>
{ {
const [ chatValue, setChatValue ] = useState<string>(''); const [ chatValue, setChatValue ] = useState<string>('');
const { chatStyleId = 0, updateChatStyleId = null } = useSessionInfo(); const { chatStyleId = 0, updateChatStyleId = null, chatColour = '', updateChatColour = null } = useSessionInfo();
const { selectedUsername = '', floodBlocked = false, floodBlockedSeconds = 0, setIsTyping = null, setIsIdle = null, sendChat = null } = useChatInputWidget(); const { selectedUsername = '', floodBlocked = false, floodBlockedSeconds = 0, setIsTyping = null, setIsIdle = null, sendChat = null } = useChatInputWidget();
const [ showInfoHabboPages, setShowInfohabboPages ] = useState<boolean>(false);
const { roomSession = null } = useRoom(); const { roomSession = null } = useRoom();
const inputRef = useRef<HTMLInputElement>();
const inputRef = useRef<HTMLInputElement>();
const chatModeIdWhisper = useMemo(() => LocalizeText('widgets.chatinput.mode.whisper'), []); const chatModeIdWhisper = useMemo(() => LocalizeText('widgets.chatinput.mode.whisper'), []);
const chatModeIdShout = useMemo(() => LocalizeText('widgets.chatinput.mode.shout'), []); const chatModeIdShout = useMemo(() => LocalizeText('widgets.chatinput.mode.shout'), []);
const chatModeIdSpeak = useMemo(() => LocalizeText('widgets.chatinput.mode.speak'), []); const chatModeIdSpeak = useMemo(() => LocalizeText('widgets.chatinput.mode.speak'), []);
@ -97,12 +101,12 @@ export const ChatInputView: FC<{}> = props =>
else else
{ {
setChatValue(''); setChatValue('');
sendChat(text, chatType, recipientName, chatStyleId); sendChat(text, chatType, recipientName, chatStyleId, chatColour);
} }
} }
setChatValue(append); setChatValue(append);
}, [ chatModeIdWhisper, chatModeIdShout, chatModeIdSpeak, maxChatLength, chatStyleId, setIsTyping, setIsIdle, sendChat ]); }, [ chatModeIdWhisper, chatModeIdShout, chatModeIdSpeak, maxChatLength, chatStyleId, setIsTyping, setIsIdle, sendChat, chatColour ]);
const updateChatInput = useCallback((value: string) => const updateChatInput = useCallback((value: string) =>
{ {
@ -214,6 +218,11 @@ export const ChatInputView: FC<{}> = props =>
return styleIds; return styleIds;
}, []); }, []);
const addEmojiToChat = (emoji: string) => {
setChatValue(chatValue + emoji);
setIsTyping(true);
};
useEffect(() => useEffect(() =>
{ {
document.body.addEventListener('keydown', onKeyDownEvent); document.body.addEventListener('keydown', onKeyDownEvent);
@ -232,17 +241,33 @@ export const ChatInputView: FC<{}> = props =>
}, [ chatValue ]); }, [ chatValue ]);
if(!roomSession || roomSession.isSpectator) return null; if(!roomSession || roomSession.isSpectator) return null;
return ( return (
createPortal( createPortal(
<div className="nitro-chat-input-container"> <div className="nitro-chat-input-container" onMouseEnter={ () => setShowInfohabboPages(true) } onMouseLeave={ () => setTimeout(() => setShowInfohabboPages(false), 100) }>
<div className="input-sizer align-items-center"> <div className="input-sizer align-items-center">
{ !floodBlocked && { !floodBlocked &&
<input ref={ inputRef } type="text" className="chat-input" placeholder={ LocalizeText('widgets.chatinput.default') } value={ chatValue } maxLength={ maxChatLength } onChange={ event => updateChatInput(event.target.value) } onMouseDown={ event => setInputFocus() } /> } <input
ref={ inputRef }
type="text"
className="chat-input"
placeholder={ LocalizeText('widgets.chatinput.default') }
value={ chatValue }
maxLength={ maxChatLength }
onChange={ event => updateChatInput(event.target.value) }
onMouseDown={ event => setInputFocus() } />
}
{ floodBlocked && { floodBlocked &&
<Text variant="danger">{ LocalizeText('chat.input.alert.flood', [ 'time' ], [ floodBlockedSeconds.toString() ]) } </Text> } <Text variant="danger">{ LocalizeText('chat.input.alert.flood', [ 'time' ], [ floodBlockedSeconds.toString() ]) } </Text> }
</div> </div>
<ChatInputStyleSelectorView chatStyleId={ chatStyleId } chatStyleIds={ chatStyleIds } selectChatStyleId={ updateChatStyleId } /> <Flex>
</div>, document.getElementById('toolbar-chat-input-container')) <ChatInputEmojiSelectorView addChatEmoji={ addEmojiToChat } />
<ChatInputColorSelectorView chatColour={ chatColour } selectColour={ updateChatColour } />
<ChatInputStyleSelectorView chatStyleId={ chatStyleId } chatStyleIds={ chatStyleIds } selectChatStyleId={ updateChatStyleId } />
{
(showInfoHabboPages) && <Base className="info-habbopages" onClick={ () => CreateLinkEvent('habbopages/chat/chatting') }></Base>
}
</Flex>
</div>, document.getElementById('toolbar-chat-input-container'))
); );
} }

View File

@ -7,11 +7,11 @@ interface ChatWidgetMessageViewProps
chat: ChatBubbleMessage; chat: ChatBubbleMessage;
makeRoom: (chat: ChatBubbleMessage) => void; makeRoom: (chat: ChatBubbleMessage) => void;
bubbleWidth?: number; bubbleWidth?: number;
selectedEmoji?: string;
} }
export const ChatWidgetMessageView: FC<ChatWidgetMessageViewProps> = props => export const ChatWidgetMessageView: FC<ChatWidgetMessageViewProps> = props => {
{ const { chat = null, makeRoom = null, bubbleWidth = RoomChatSettings.CHAT_BUBBLE_WIDTH_NORMAL, selectedEmoji } = props;
const { chat = null, makeRoom = null, bubbleWidth = RoomChatSettings.CHAT_BUBBLE_WIDTH_NORMAL } = props;
const [ isVisible, setIsVisible ] = useState(false); const [ isVisible, setIsVisible ] = useState(false);
const [ isReady, setIsReady ] = useState<boolean>(false); const [ isReady, setIsReady ] = useState<boolean>(false);
const elementRef = useRef<HTMLDivElement>(); const elementRef = useRef<HTMLDivElement>();
@ -76,8 +76,9 @@ export const ChatWidgetMessageView: FC<ChatWidgetMessageViewProps> = props =>
}, [ chat, isReady, isVisible, makeRoom ]); }, [ chat, isReady, isVisible, makeRoom ]);
return ( return (
<div ref={ elementRef } className={ `bubble-container ${ isVisible ? 'visible' : 'invisible' }` } onClick={ event => GetRoomEngine().selectRoomObject(chat.roomId, chat.senderId, RoomObjectCategory.UNIT) }> <div ref={elementRef} className={`bubble-container newbubblehe ${isVisible ? 'visible' : 'invisible'}`} onClick={event => GetRoomEngine().selectRoomObject(chat.roomId, chat.senderId, RoomObjectCategory.UNIT)}>
{ (chat.styleId === 0) && {selectedEmoji && <span>{DOMPurify.sanitize(selectedEmoji)}</span>}
{ (chat.styleId === 0) &&
<div className="user-container-bg" style={ { backgroundColor: chat.color } } /> } <div className="user-container-bg" style={ { backgroundColor: chat.color } } /> }
<div className={ `chat-bubble bubble-${ chat.styleId } type-${ chat.type }` } style={ { maxWidth: getBubbleWidth } }> <div className={ `chat-bubble bubble-${ chat.styleId } type-${ chat.type }` } style={ { maxWidth: getBubbleWidth } }>
<div className="user-container"> <div className="user-container">
@ -86,10 +87,10 @@ export const ChatWidgetMessageView: FC<ChatWidgetMessageViewProps> = props =>
</div> </div>
<div className="chat-content"> <div className="chat-content">
<b className="username mr-1" dangerouslySetInnerHTML={ { __html: `${ chat.username }: ` } } /> <b className="username mr-1" dangerouslySetInnerHTML={ { __html: `${ chat.username }: ` } } />
<span className="message" dangerouslySetInnerHTML={ { __html: `${ chat.formattedText }` } } /> <span className="message" style={{ color: chat.chatColours }} dangerouslySetInnerHTML={{ __html: `${chat.formattedText}` }} onClick={e => onClickChat(e)} />
</div> </div>
<div className="pointer" /> <div className="pointer" />
</div> </div>
</div> </div>
); );
} }

View File

@ -1,6 +1,6 @@
import { CreateLinkEvent, Dispose, DropBounce, EaseOut, GetConfiguration, GetSessionDataManager, JumpBy, Motions, NitroToolbarAnimateIconEvent, PerkAllowancesMessageEvent, PerkEnum, Queue, Wait } from '@nitrots/nitro-renderer'; import { CreateLinkEvent, Dispose, DropBounce, EaseOut, GetSessionDataManager, JumpBy, Motions, NitroToolbarAnimateIconEvent, PerkAllowancesMessageEvent, PerkEnum, Queue, Wait } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react'; import { FC, useState } from 'react';
import { MessengerIconState, OpenMessengerChat, VisitDesktop } from '../../api'; import { GetConfigurationValue, MessengerIconState, OpenMessengerChat, VisitDesktop } from '../../api';
import { Base, Flex, LayoutAvatarImageView, LayoutItemCountView, TransitionAnimation, TransitionAnimationTypes } from '../../common'; import { Base, Flex, LayoutAvatarImageView, LayoutItemCountView, TransitionAnimation, TransitionAnimationTypes } from '../../common';
import { useAchievements, useFriends, useInventoryUnseenTracker, useMessageEvent, useMessenger, useNitroEvent, useSessionInfo } from '../../hooks'; import { useAchievements, useFriends, useInventoryUnseenTracker, useMessageEvent, useMessenger, useNitroEvent, useSessionInfo } from '../../hooks';
import { ToolbarMeView } from './ToolbarMeView'; import { ToolbarMeView } from './ToolbarMeView';
@ -81,7 +81,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
{ !isInRoom && { !isInRoom &&
<Base pointer className="navigation-item icon icon-house click-box" onClick={ event => CreateLinkEvent('navigator/goto/home') } /> } <Base pointer className="navigation-item icon icon-house click-box" onClick={ event => CreateLinkEvent('navigator/goto/home') } /> }
<Base pointer className="navigation-item icon icon-rooms click-box" onClick={ event => CreateLinkEvent('navigator/toggle') } /> <Base pointer className="navigation-item icon icon-rooms click-box" onClick={ event => CreateLinkEvent('navigator/toggle') } />
{ GetConfiguration('game.center.enabled') && <Base pointer className="navigation-item icon icon-game click-box" onClick={ event => CreateLinkEvent('games/toggle') } /> } { GetConfigurationValue('game.center.enabled') && <Base pointer className="navigation-item icon icon-game click-box" onClick={ event => CreateLinkEvent('games/toggle') } /> }
<Base pointer className="navigation-item icon icon-catalog click-box" onClick={ event => CreateLinkEvent('catalog/toggle') } /> <Base pointer className="navigation-item icon icon-catalog click-box" onClick={ event => CreateLinkEvent('catalog/toggle') } />
<Base pointer className="navigation-item icon icon-inventory click-box" onClick={ event => CreateLinkEvent('inventory/toggle') }> <Base pointer className="navigation-item icon icon-inventory click-box" onClick={ event => CreateLinkEvent('inventory/toggle') }>
{ (getFullCount > 0) && { (getFullCount > 0) &&

View File

@ -3,6 +3,7 @@ import { useState } from 'react';
import { useBetween } from 'use-between'; import { useBetween } from 'use-between';
import { ChatEntryType, ChatHistoryCurrentDate, IChatEntry, IRoomHistoryEntry, MessengerHistoryCurrentDate } from '../../api'; import { ChatEntryType, ChatHistoryCurrentDate, IChatEntry, IRoomHistoryEntry, MessengerHistoryCurrentDate } from '../../api';
import { useMessageEvent, useNitroEvent } from '../events'; import { useMessageEvent, useNitroEvent } from '../events';
import { useLocalStorage } from '../useLocalStorage';
const CHAT_HISTORY_MAX = 1000; const CHAT_HISTORY_MAX = 1000;
const ROOM_HISTORY_MAX = 10; const ROOM_HISTORY_MAX = 10;
@ -13,10 +14,10 @@ let MESSENGER_HISTORY_COUNTER: number = 0;
const useChatHistoryState = () => const useChatHistoryState = () =>
{ {
const [ chatHistory, setChatHistory ] = useState<IChatEntry[]>([]); const [ chatHistory, setChatHistory ] = useLocalStorage<IChatEntry[]>('chatHistory', []);
const [ roomHistory, setRoomHistory ] = useState<IRoomHistoryEntry[]>([]); const [ roomHistory, setRoomHistory ] = useLocalStorage<IRoomHistoryEntry[]>('roomHistory', []);
const [ messengerHistory, setMessengerHistory ] = useState<IChatEntry[]>([]); const [ messengerHistory, setMessengerHistory ] = useLocalStorage<IChatEntry[]>('messengerHistory', []);
const [ needsRoomInsert, setNeedsRoomInsert ] = useState(false); const [ needsRoomInsert, setNeedsRoomInsert ] = useLocalStorage('needsRoomInsert', false);
const addChatEntry = (entry: IChatEntry) => const addChatEntry = (entry: IChatEntry) =>
{ {

View File

@ -0,0 +1,4 @@
import { GetRoomSessionManager, NitroEvent } from '@nitrots/nitro-renderer';
import { useEventDispatcher } from './useEventDispatcher';
export const useRoomSessionManagerEvent = <T extends NitroEvent>(type: string | string[], handler: (event: T) => void) => useEventDispatcher(type, GetRoomSessionManager().events, handler);

View File

@ -17,7 +17,7 @@ const useChatInputWidgetState = () =>
const { showNitroAlert = null, showConfirm = null } = useNotification(); const { showNitroAlert = null, showConfirm = null } = useNotification();
const { roomSession = null } = useRoom(); const { roomSession = null } = useRoom();
const sendChat = (text: string, chatType: number, recipientName: string = '', styleId: number = 0) => const sendChat = (text: string, chatType: number, recipientName: string = '', styleId: number = 0, chatColour: string = '') =>
{ {
if(text === '') return null; if(text === '') return null;
@ -108,8 +108,17 @@ const useChatInputWidgetState = () =>
return null; return null;
case ':zoom': case ':zoom':
GetEventDispatcher().dispatchEvent(new RoomZoomEvent(roomSession.roomId, parseFloat(secondPart), false)); let requestedZoomLevel = parseFloat(secondPart);
if (isNaN(requestedZoomLevel)) {
requestedZoomLevel = 1;
}
if (requestedZoomLevel >= 1 && requestedZoomLevel <= 5) {
GetRoomEngine().events.dispatchEvent(new RoomZoomEvent(roomSession.roomId, requestedZoomLevel, false));
} else if (requestedZoomLevel === 0) {
GetRoomEngine().events.dispatchEvent(new RoomZoomEvent(roomSession.roomId, 1, false));
} else {
GetRoomEngine().events.dispatchEvent(new RoomZoomEvent(roomSession.roomId, 1, false));
};
return null; return null;
case ':screenshot': case ':screenshot':
const texture = GetRoomEngine().createTextureFromRoom(roomSession.roomId, 1); const texture = GetRoomEngine().createTextureFromRoom(roomSession.roomId, 1);
@ -131,7 +140,7 @@ const useChatInputWidgetState = () =>
{ {
GetSessionDataManager().sendSpecialCommandMessage(':pickall'); GetSessionDataManager().sendSpecialCommandMessage(':pickall');
}, },
null, null, null, LocalizeText('generic.alert.title')); null, null, null, LocalizeText('generic.alert.title'), null, 'pickall');
} }
return null; return null;
@ -180,10 +189,10 @@ const useChatInputWidgetState = () =>
switch(chatType) switch(chatType)
{ {
case ChatMessageTypeEnum.CHAT_DEFAULT: case ChatMessageTypeEnum.CHAT_DEFAULT:
roomSession.sendChatMessage(text, styleId); roomSession.sendChatMessage(text, styleId, chatColour);
break; break;
case ChatMessageTypeEnum.CHAT_SHOUT: case ChatMessageTypeEnum.CHAT_SHOUT:
roomSession.sendShoutMessage(text, styleId); roomSession.sendChatMessage(text, styleId, chatColour);
break; break;
case ChatMessageTypeEnum.CHAT_WHISPER: case ChatMessageTypeEnum.CHAT_WHISPER:
roomSession.sendWhisperMessage(recipientName, text, styleId); roomSession.sendWhisperMessage(recipientName, text, styleId);
@ -216,7 +225,7 @@ const useChatInputWidgetState = () =>
let seconds = 0; let seconds = 0;
const interval = setInterval(() => const interval = window.setInterval(() =>
{ {
setFloodBlockedSeconds(prevValue => setFloodBlockedSeconds(prevValue =>
{ {

View File

@ -48,6 +48,7 @@ const useChatWidgetState = () =>
let userType = 0; let userType = 0;
let petType = -1; let petType = -1;
let text = event.message; let text = event.message;
let chatColours = event._chatColours
if(userData) if(userData)
{ {
@ -60,6 +61,7 @@ const useChatWidgetState = () =>
case RoomObjectType.PET: case RoomObjectType.PET:
imageUrl = await ChatBubbleUtilities.getPetImage(figure, 2, true, 64, roomObject.model.getValue<string>(RoomObjectVariable.FIGURE_POSTURE)); imageUrl = await ChatBubbleUtilities.getPetImage(figure, 2, true, 64, roomObject.model.getValue<string>(RoomObjectVariable.FIGURE_POSTURE));
petType = new PetFigureData(figure).typeId; petType = new PetFigureData(figure).typeId;
chatColours = "black"
break; break;
case RoomObjectType.USER: case RoomObjectType.USER:
imageUrl = await ChatBubbleUtilities.getUserImage(figure); imageUrl = await ChatBubbleUtilities.getUserImage(figure);
@ -67,6 +69,7 @@ const useChatWidgetState = () =>
case RoomObjectType.RENTABLE_BOT: case RoomObjectType.RENTABLE_BOT:
case RoomObjectType.BOT: case RoomObjectType.BOT:
styleId = SystemChatStyleEnum.BOT; styleId = SystemChatStyleEnum.BOT;
chatColours = "black"
break; break;
} }
@ -144,10 +147,11 @@ const useChatWidgetState = () =>
chatType, chatType,
styleId, styleId,
imageUrl, imageUrl,
color); color,
chatColours);
setChatMessages(prevValue => [ ...prevValue, chatMessage ]); setChatMessages(prevValue => [ ...prevValue, chatMessage ]);
addChatEntry({ id: -1, webId: userData.webID, entityId: userData.roomIndex, name: username, imageUrl, style: styleId, chatType: chatType, entityType: userData.type, message: formattedText, timestamp: ChatHistoryCurrentDate(), type: ChatEntryType.TYPE_CHAT, roomId: roomSession.roomId, color }); addChatEntry({ id: -1, webId: userData.webID, entityId: userData.roomIndex, name: username, imageUrl, style: styleId, chatType: chatType, entityType: userData.type, message: formattedText, timestamp: ChatHistoryCurrentDate(), type: ChatEntryType.TYPE_CHAT, roomId: roomSession.roomId, color, chatColours });
}); });
useNitroEvent<RoomDragEvent>(RoomDragEvent.ROOM_DRAG, event => useNitroEvent<RoomDragEvent>(RoomDragEvent.ROOM_DRAG, event =>
@ -188,4 +192,4 @@ const useChatWidgetState = () =>
return { chatMessages, setChatMessages, chatSettings, getScrollSpeed }; return { chatMessages, setChatMessages, chatSettings, getScrollSpeed };
} }
export const useChatWidget = useChatWidgetState; export const useChatWidget = useChatWidgetState;

View File

@ -1,14 +1,16 @@
import { FigureUpdateEvent, GetSessionDataManager, RoomUnitChatStyleComposer, UserInfoDataParser, UserInfoEvent, UserSettingsEvent } from '@nitrots/nitro-renderer'; import { FigureUpdateEvent, GetSessionDataManager, RoomUnitChatStyleComposer, UserInfoDataParser, UserInfoEvent, UserSettingsEvent } from '@nitrots/nitro-renderer';
import { useState } from 'react'; import { useState } from 'react';
import { useBetween } from 'use-between'; import { useBetween } from 'use-between';
import { SendMessageComposer } from '../../api'; import { GetLocalStorage, SendMessageComposer } from '../../api';
import { useMessageEvent } from '../events'; import { useMessageEvent } from '../events';
import { useLocalStorage } from '../useLocalStorage';
const useSessionInfoState = () => const useSessionInfoState = () =>
{ {
const [ userInfo, setUserInfo ] = useState<UserInfoDataParser>(null); const [ userInfo, setUserInfo ] = useState<UserInfoDataParser>(null);
const [ userFigure, setUserFigure ] = useState<string>(null); const [ userFigure, setUserFigure ] = useState<string>(null);
const [ chatStyleId, setChatStyleId ] = useState<number>(0); const [ chatStyleId, setChatStyleId ] = useLocalStorage<number>(0);
const [ chatColour, setChatColour ] = useLocalStorage<string>('chatColour', '');
const [ userRespectRemaining, setUserRespectRemaining ] = useState<number>(0); const [ userRespectRemaining, setUserRespectRemaining ] = useState<number>(0);
const [ petRespectRemaining, setPetRespectRemaining ] = useState<number>(0); const [ petRespectRemaining, setPetRespectRemaining ] = useState<number>(0);
@ -18,6 +20,11 @@ const useSessionInfoState = () =>
SendMessageComposer(new RoomUnitChatStyleComposer(styleId)); SendMessageComposer(new RoomUnitChatStyleComposer(styleId));
} }
const updateChatColour = (colour: string) =>
{
setChatColour(colour);
}
const respectUser = (userId: number) => const respectUser = (userId: number) =>
{ {
@ -57,7 +64,7 @@ const useSessionInfoState = () =>
setChatStyleId(parser.chatType); setChatStyleId(parser.chatType);
}); });
return { userInfo, userFigure, chatStyleId, userRespectRemaining, petRespectRemaining, respectUser, respectPet, updateChatStyleId }; return { userInfo, userFigure, chatStyleId, userRespectRemaining, petRespectRemaining, respectUser, respectPet, updateChatStyleId, updateChatColour, chatColour };
} }
export const useSessionInfo = () => useBetween(useSessionInfoState); export const useSessionInfo = () => useBetween(useSessionInfoState);

View File

@ -2,16 +2,17 @@ import { NitroLogger } from '@nitrots/nitro-renderer';
import { Dispatch, SetStateAction, useState } from 'react'; import { Dispatch, SetStateAction, useState } from 'react';
import { GetLocalStorage, SetLocalStorage } from '../api'; import { GetLocalStorage, SetLocalStorage } from '../api';
const userId = new URLSearchParams(window.location.search).get('userid') || 0;
const useLocalStorageState = <T>(key: string, initialValue: T): [ T, Dispatch<SetStateAction<T>>] => const useLocalStorageState = <T>(key: string, initialValue: T): [ T, Dispatch<SetStateAction<T>>] =>
{ {
key = userId ? `${ key }.${ userId }` : key;
const [ storedValue, setStoredValue ] = useState<T>(() => const [ storedValue, setStoredValue ] = useState<T>(() =>
{ {
if(typeof window === 'undefined') return initialValue;
try try
{ {
const item = GetLocalStorage<T>(key); const item = typeof window !== 'undefined' ? GetLocalStorage<T>(key) as T : undefined;
return item ?? initialValue; return item ?? initialValue;
} }