Merge branch 'dev' into 'main'

Release 2.1.1

Closes #252

See merge request nitro/nitro-react!74
This commit is contained in:
Bill 2022-08-18 16:32:16 +00:00
commit 568e7bc002
69 changed files with 866 additions and 348 deletions

View File

@ -1,6 +1,7 @@
{
"name": "nitro-react",
"version": "2.1.0",
"version": "2.1.1",
"homepage": ".",
"private": true,
"scripts": {
"start": "cross-env SKIP_PREFLIGHT_CHECK=true BROWSER=none IMAGE_INLINE_SIZE_LIMIT=100000 craco --openssl-legacy-provider start",

View File

@ -15,6 +15,7 @@
<meta name="theme-color" content="#000000" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<base href="./">
<title>Nitro</title>
</head>
<body>

View File

@ -16,6 +16,11 @@
"badge.descriptions.enabled": true,
"motto.max.length": 38,
"bot.name.max.length": 15,
"wired.action.bot.talk.to.avatar.max.length": 64,
"wired.action.bot.talk.max.length": 64,
"wired.action.chat.max.length": 100,
"wired.action.kick.from.room.max.length": 100,
"wired.action.mute.user.max.length": 100,
"navigator.room.models": [
{ "clubLevel": 0, "tileSize": 104, "name": "a" },
{ "clubLevel": 0, "tileSize": 94, "name": "b" },
@ -120,6 +125,8 @@
"catalog.asset.url": "${image.library.url}catalogue",
"catalog.asset.image.url": "${catalog.asset.url}/%name%.gif",
"catalog.asset.icon.url": "${catalog.asset.url}/icon_%name%.png",
"catalog.tab.icons": false,
"catalog.headers": false,
"chat.input.maxlength": 100,
"chat.styles.disabled": [],
"chat.styles": [

View File

@ -1 +1 @@
export const GetUIVersion = () => '2.1.0';
export const GetUIVersion = () => '2.1.1';

View File

@ -0,0 +1,9 @@
export const ConvertSeconds = (seconds: number) =>
{
let numDays = Math.floor(seconds / 86400);
let numHours = Math.floor((seconds % 86400) / 3600);
let numMinutes = Math.floor(((seconds % 86400) % 3600) / 60);
let numSeconds = ((seconds % 86400) % 3600) % 60;
return numDays.toString().padStart(2, '0') + ':' + numHours.toString().padStart(2, '0') + ':' + numMinutes.toString().padStart(2, '0') + ':' + numSeconds.toString().padStart(2, '0');
}

View File

@ -0,0 +1 @@
export const GetLocalStorage = <T>(key: string) => JSON.parse(window.localStorage.getItem(key)) as T ?? null;

View File

@ -0,0 +1 @@
export const SetLocalStorage = <T>(key: string, value: T) => window.localStorage.setItem(key, JSON.stringify(value));

View File

@ -0,0 +1,5 @@
export interface WindowSaveOptions
{
offset: { x: number, y: number };
size: { width: number, height: number };
}

View File

@ -1,5 +1,7 @@
export * from './CloneObject';
export * from './ColorUtils';
export * from './ConvertSeconds';
export * from './GetLocalStorage';
export * from './LocalizeBadgeDescription';
export * from './LocalizeBageName';
export * from './LocalizeFormattedNumber';
@ -10,4 +12,6 @@ export * from './PlaySound';
export * from './ProductImageUtility';
export * from './Randomizer';
export * from './RoomChatFormatter';
export * from './SetLocalStorage';
export * from './SoundNames';
export * from './WindowSaveOptions';

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -1,5 +1,6 @@
import { FC, useMemo } from 'react';
import { FC, useEffect, useMemo, useRef } from 'react';
import { Column, ColumnProps } from '..';
import { GetLocalStorage, SetLocalStorage, WindowSaveOptions } from '../../api';
import { DraggableWindow, DraggableWindowPosition, DraggableWindowProps } from '../draggable-window';
import { NitroCardContextProvider } from './NitroCardContext';
@ -11,6 +12,7 @@ export interface NitroCardViewProps extends DraggableWindowProps, ColumnProps
export const NitroCardView: FC<NitroCardViewProps> = props =>
{
const { theme = 'primary', uniqueKey = null, handleSelector = '.drag-handler', windowPosition = DraggableWindowPosition.CENTER, disableDrag = false, overflow = 'hidden', position = 'relative', gap = 0, classNames = [], ...rest } = props;
const elementRef = useRef<HTMLDivElement>();
const getClassNames = useMemo(() =>
{
@ -23,10 +25,40 @@ export const NitroCardView: FC<NitroCardViewProps> = props =>
return newClassNames;
}, [ theme, classNames ]);
useEffect(() =>
{
if(!uniqueKey || !elementRef || !elementRef.current) return;
const localStorage = GetLocalStorage<WindowSaveOptions>(`nitro.windows.${ uniqueKey }`);
const element = elementRef.current;
if(localStorage && localStorage.size)
{
element.style.width = `${ localStorage.size.width }px`;
element.style.height = `${ localStorage.size.height }px`;
}
const observer = new ResizeObserver(event =>
{
const newStorage = { ...GetLocalStorage<Partial<WindowSaveOptions>>(`nitro.windows.${ uniqueKey }`) } as WindowSaveOptions;
newStorage.size = { width: element.offsetWidth, height: element.offsetHeight };
SetLocalStorage<WindowSaveOptions>(`nitro.windows.${ uniqueKey }`, newStorage);
});
observer.observe(element);
return () =>
{
observer.disconnect();
}
}, [ uniqueKey ]);
return (
<NitroCardContextProvider value={ { theme } }>
<DraggableWindow uniqueKey={ uniqueKey } handleSelector={ handleSelector } windowPosition={ windowPosition } disableDrag={ disableDrag }>
<Column overflow={ overflow } position={ position } gap={ gap } classNames={ getClassNames } { ...rest } />
<Column innerRef={ elementRef } overflow={ overflow } position={ position } gap={ gap } classNames={ getClassNames } { ...rest } />
</DraggableWindow>
</NitroCardContextProvider>
);

View File

@ -2,10 +2,10 @@ import { MouseEventType, TouchEventType } from '@nitrots/nitro-renderer';
import { CSSProperties, FC, Key, MouseEvent as ReactMouseEvent, ReactNode, TouchEvent as ReactTouchEvent, useCallback, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Base } from '..';
import { GetLocalStorage, SetLocalStorage, WindowSaveOptions } from '../../api';
import { DraggableWindowPosition } from './DraggableWindowPosition';
const CURRENT_WINDOWS: HTMLElement[] = [];
const POS_MEMORY: Map<Key, { x: number, y: number }> = new Map();
const BOUNDS_THRESHOLD_TOP: number = 0;
const BOUNDS_THRESHOLD_LEFT: number = 0;
@ -138,7 +138,14 @@ export const DraggableWindow: FC<DraggableWindowProps> = props =>
setOffset({ x: offsetX, y: offsetY });
setIsDragging(false);
if(uniqueKey !== null) POS_MEMORY.set(uniqueKey, { x: offsetX, y: offsetY });
if(uniqueKey !== null)
{
const newStorage = { ...GetLocalStorage<WindowSaveOptions>(`nitro.windows.${ uniqueKey }`) } as WindowSaveOptions;
newStorage.offset = { x: offsetX, y: offsetY };
SetLocalStorage<WindowSaveOptions>(`nitro.windows.${ uniqueKey }`, newStorage);
}
}, [ dragHandler, delta, offset, uniqueKey ]);
const onDragMouseUp = useCallback((event: MouseEvent) =>
@ -187,17 +194,6 @@ export const DraggableWindow: FC<DraggableWindowProps> = props =>
break;
}
if(uniqueKey !== null)
{
const memory = POS_MEMORY.get(uniqueKey);
if(memory)
{
offsetX = memory.x;
offsetY = memory.y;
}
}
setDelta({ x: 0, y: 0 });
setOffset({ x: offsetX, y: offsetY });
@ -253,6 +249,18 @@ export const DraggableWindow: FC<DraggableWindowProps> = props =>
}
}, [ isDragging, onDragMouseUp, onDragMouseMove, onDragTouchUp, onDragTouchMove ]);
useEffect(() =>
{
if(!uniqueKey) return;
const localStorage = GetLocalStorage<WindowSaveOptions>(`nitro.windows.${ uniqueKey }`);
if(!localStorage || !localStorage.offset) return;
setDelta({ x: 0, y: 0 });
if(localStorage.offset) setOffset(localStorage.offset);
}, [ uniqueKey ]);
return (
createPortal(
<Base position="absolute" innerRef={ elementRef } className="draggable-window" onMouseDownCapture={ onMouseDown } onTouchStartCapture={ onTouchStart } style={ dragStyle }>

View File

@ -5,10 +5,10 @@
background-color: $grid-bg-color;
&.active {
border-color: $grid-active-border-color !important;
border-color: $grid-active-border-color !important;
&:not(.clear-bg) {
background-color: $grid-active-bg-color !important;
background-color: $grid-active-bg-color !important;
}
}
@ -53,7 +53,7 @@
position: absolute;
width: 110px;
height: 110px;
margin-top: 38px;
margin-top: 30px;
margin-left: 3px;
}
}
@ -110,8 +110,7 @@
.gift-incognito {
width: 37px;
height: 48px;
background: url("../assets/images/gift/incognito.png") center
no-repeat;
background: url("../assets/images/gift/incognito.png") center no-repeat;
}
.gift-avatar {
@ -168,23 +167,27 @@
}
@-webkit-keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
}
40% {
-webkit-transform: scale(1);
}
}
@keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
transform: scale(0);
}
40% {
-webkit-transform: scale(1);
transform: scale(1);
@ -195,8 +198,7 @@
position: relative;
width: 110px;
height: 110px;
background: url("../assets/images/navigator/thumbnail_placeholder.png")
no-repeat center;
background: url("../assets/images/navigator/thumbnail_placeholder.png") no-repeat center;
background-color: rgba($black, 0.125);
}
@ -299,6 +301,19 @@
}
}
.nitro-counter-time {
width: 36px;
height: 28px;
background: url("../assets/images/infostand/countown-timer.png");
div {
line-height: 28px;
text-align: center;
color: $white;
font-weight: bold;
}
}
.avatar-image {
position: relative;
width: 90px;
@ -375,15 +390,13 @@
content: "";
width: 100%;
height: 100%;
background: url("../assets/images/unique/grid-bg-glass.png") center
no-repeat;
background: url("../assets/images/unique/grid-bg-glass.png") center no-repeat;
bottom: 0;
z-index: 4;
}
&.sold-out:after {
background: url("../assets/images/unique/grid-bg-sold-out.png") center
no-repeat,
background: url("../assets/images/unique/grid-bg-sold-out.png") center no-repeat,
url("../assets/images/unique/grid-bg-glass.png") center no-repeat;
}
@ -395,8 +408,7 @@
bottom: 1px;
width: 100%;
height: 9px;
background: url("../assets/images/unique/grid-count-bg.png") center
no-repeat;
background: url("../assets/images/unique/grid-count-bg.png") center no-repeat;
z-index: 3;
}
}
@ -438,8 +450,7 @@
.unique-complete-plate {
width: 170px;
height: 29px;
background: url("../assets/images/unique/catalog-info-amount-bg.png")
no-repeat center;
background: url("../assets/images/unique/catalog-info-amount-bg.png") no-repeat center;
z-index: 1;
padding-top: 3px;
@ -535,12 +546,10 @@
z-index: 1;
transition: all 1s;
border-radius: calc(#{$border-radius} / 2);
background: repeating-linear-gradient(
$tertiary,
$tertiary 50%,
$quaternary 50%,
$quaternary 100%
);
background: repeating-linear-gradient($tertiary,
$tertiary 50%,
$quaternary 50%,
$quaternary 100%);
}
.nitro-progress-bar-text {

View File

@ -0,0 +1,43 @@
import { FC, useMemo } from 'react';
import { LocalizeText } from '../../api';
import { Base, BaseProps } from '../Base';
import { Flex } from '../Flex';
interface LayoutCounterTimeViewProps extends BaseProps<HTMLDivElement>
{
day: string;
hour: string;
minutes: string;
seconds: string;
}
export const LayoutCounterTimeView: FC<LayoutCounterTimeViewProps> = props =>
{
const { day = '00', hour = '00', minutes = '00', seconds = '00', classNames = [], children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'nitro-counter-time' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return (
<Flex gap={ 1 }>
<Base classNames={ getClassNames } { ...rest }>
<div>{ day != '00' ? day : hour }{ day != '00' ? LocalizeText('countdown_clock_unit_days') : LocalizeText('countdown_clock_unit_hours') }</div>
</Base>
<Base style={ { marginTop: '3px' } }>:</Base>
<Base classNames={ getClassNames } { ...rest }>
<div>{ minutes }{ LocalizeText('countdown_clock_unit_minutes') }</div>
</Base>
<Base style={ { marginTop: '3px' } }>:</Base>
<Base classNames={ getClassNames } { ...rest }>
<div>{ seconds }{ LocalizeText('countdown_clock_unit_seconds') }</div>
</Base>
{ children }
</Flex>
);
}

View File

@ -20,6 +20,8 @@ export const LayoutPetImageView: FC<LayoutPetImageViewProps> = props =>
{
const { figure = '', typeId = -1, paletteId = -1, petColor = 0xFFFFFF, customParts = [], posture = 'std', headOnly = false, direction = 0, scale = 1, style = {}, ...rest } = props;
const [ petUrl, setPetUrl ] = useState<string>(null);
const [ width, setWidth ] = useState(0);
const [ height, setHeight ] = useState(0);
const isDisposed = useRef(false);
const getStyle = useMemo(() =>
@ -35,10 +37,13 @@ export const LayoutPetImageView: FC<LayoutPetImageViewProps> = props =>
if(!(scale % 1)) newStyle.imageRendering = 'pixelated';
}
newStyle.width = width;
newStyle.height = height;
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ petUrl, scale, style ]);
}, [ petUrl, scale, style, width, height ]);
useEffect(() =>
{
@ -67,8 +72,19 @@ export const LayoutPetImageView: FC<LayoutPetImageViewProps> = props =>
{
if(isDisposed.current) return;
if(image) setPetUrl(image.src);
else if(texture) setPetUrl(TextureUtils.generateImageUrl(texture));
if(image)
{
setPetUrl(image.src);
setWidth(image.width);
setHeight(image.height);
}
else if(texture)
{
setPetUrl(TextureUtils.generateImageUrl(texture));
setWidth(texture.width);
setHeight(texture.height);
}
},
imageFailed: (id) =>
{
@ -80,9 +96,12 @@ export const LayoutPetImageView: FC<LayoutPetImageViewProps> = props =>
{
const image = imageResult.getImage();
if(image) setPetUrl(image.src);
if(image)
{
setPetUrl(image.src);
setWidth(image.width);
setHeight(image.height);
}
}
}, [ figure, typeId, paletteId, petColor, customParts, posture, headOnly, direction ]);

View File

@ -1,6 +1,7 @@
export * from './LayoutAvatarImageView';
export * from './LayoutBackgroundImage';
export * from './LayoutBadgeImageView';
export * from './LayoutCounterTimeView';
export * from './LayoutCurrencyIcon';
export * from './LayoutFurniIconImageView';
export * from './LayoutFurniImageView';

View File

@ -1,4 +1,4 @@
import { FC, useCallback, useEffect, useState } from 'react';
import { FC, useEffect, useState } from 'react';
import { AvatarEditorGridPartItem, GetConfiguration } from '../../../../api';
import { LayoutCurrencyIcon, LayoutGridItem, LayoutGridItemProps } from '../../../../common';
import { AvatarEditorIcon } from '../AvatarEditorIcon';
@ -15,20 +15,14 @@ export const AvatarEditorFigureSetItemView: FC<AvatarEditorFigureSetItemViewProp
const hcDisabled = GetConfiguration<boolean>('hc.disabled', false);
const rerender = useCallback(() =>
{
setUpdateId(prevValue => (prevValue + 1));
}, []);
useEffect(() =>
{
const rerender = () => setUpdateId(prevValue => (prevValue + 1));
partItem.notify = rerender;
return () =>
{
partItem.notify = null;
}
}, [ partItem, rerender ]);
return () => partItem.notify = null;
}, [ partItem ]);
return (
<LayoutGridItem itemImage={ (partItem.isClear ? undefined : partItem.imageUrl) } itemActive={ partItem.isSelected } { ...rest }>

View File

@ -1,4 +1,4 @@
import { Dispatch, FC, SetStateAction, useCallback } from 'react';
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from 'react';
import { AvatarEditorGridPartItem, CategoryData, IAvatarEditorCategoryModel } from '../../../../api';
import { AutoGrid } from '../../../../common';
import { AvatarEditorFigureSetItemView } from './AvatarEditorFigureSetItemView';
@ -13,6 +13,7 @@ export interface AvatarEditorFigureSetViewProps
export const AvatarEditorFigureSetView: FC<AvatarEditorFigureSetViewProps> = props =>
{
const { model = null, category = null, setMaxPaletteCount = null } = props;
const elementRef = useRef<HTMLDivElement>(null);
const selectPart = useCallback((item: AvatarEditorGridPartItem) =>
{
@ -27,8 +28,15 @@ export const AvatarEditorFigureSetView: FC<AvatarEditorFigureSetViewProps> = pro
setMaxPaletteCount(partItem.maxColorIndex || 1);
}, [ model, category, setMaxPaletteCount ]);
useEffect(() =>
{
if(!model || !category || !elementRef || !elementRef.current) return;
elementRef.current.scrollTop = 0;
}, [ model, category ]);
return (
<AutoGrid columnCount={ 3 } columnMinHeight={ 50 }>
<AutoGrid innerRef={ elementRef } columnCount={ 3 } columnMinHeight={ 50 }>
{ (category.parts.length > 0) && category.parts.map((item, index) =>
<AvatarEditorFigureSetItemView key={ index } partItem={ item } onClick={ event => selectPart(item) } />) }
</AutoGrid>

View File

@ -1,4 +1,4 @@
import { FC, useCallback, useEffect, useState } from 'react';
import { FC, useEffect, useState } from 'react';
import { AvatarEditorGridColorItem, GetConfiguration } from '../../../../api';
import { LayoutCurrencyIcon, LayoutGridItem, LayoutGridItemProps } from '../../../../common';
@ -14,17 +14,14 @@ export const AvatarEditorPaletteSetItem: FC<AvatarEditorPaletteSetItemProps> = p
const hcDisabled = GetConfiguration<boolean>('hc.disabled', false);
const rerender = useCallback(() =>
{
setUpdateId(prevValue => (prevValue + 1));
}, []);
useEffect(() =>
{
const rerender = () => setUpdateId(prevValue => (prevValue + 1));
colorItem.notify = rerender;
return () => colorItem.notify = null;
});
}, [ colorItem ]);
return (
<LayoutGridItem itemHighlight itemColor={ colorItem.color } itemActive={ colorItem.isSelected } className="clear-bg" { ...rest }>

View File

@ -1,4 +1,4 @@
import { FC, useCallback } from 'react';
import { FC, useCallback, useEffect, useRef } from 'react';
import { AvatarEditorGridColorItem, CategoryData, IAvatarEditorCategoryModel } from '../../../../api';
import { AutoGrid } from '../../../../common';
import { AvatarEditorPaletteSetItem } from './AvatarEditorPaletteSetItemView';
@ -14,6 +14,7 @@ export interface AvatarEditorPaletteSetViewProps
export const AvatarEditorPaletteSetView: FC<AvatarEditorPaletteSetViewProps> = props =>
{
const { model = null, category = null, paletteSet = [], paletteIndex = -1 } = props;
const elementRef = useRef<HTMLDivElement>(null);
const selectColor = useCallback((item: AvatarEditorGridColorItem) =>
{
@ -24,8 +25,15 @@ export const AvatarEditorPaletteSetView: FC<AvatarEditorPaletteSetViewProps> = p
model.selectColor(category.name, index, paletteIndex);
}, [ model, category, paletteSet, paletteIndex ]);
useEffect(() =>
{
if(!model || !category || !elementRef || !elementRef.current) return;
elementRef.current.scrollTop = 0;
}, [ model, category ]);
return (
<AutoGrid gap={ 1 } columnCount={ 5 } columnMinWidth={ 30 }>
<AutoGrid innerRef={ elementRef } gap={ 1 } columnCount={ 5 } columnMinWidth={ 30 }>
{ (paletteSet.length > 0) && paletteSet.map((item, index) =>
<AvatarEditorPaletteSetItem key={ index } colorItem={ item } onClick={ event => selectColor(item) } />) }
</AutoGrid>

View File

@ -2,7 +2,7 @@
width: $catalog-width;
height: $catalog-height;
font[size='16'] {
font[size="16"] {
font-size: 20px;
}
@ -22,7 +22,7 @@
.nitro-catalog-gift {
width: 325px;
.gift-preview {
width: 80px;
height: 80px;
@ -37,15 +37,24 @@
}
.nitro-catalog-navigation-grid-container {
border-color: #B6BEC5 !important;
background-color: #CDD3D9;
border-color: #b6bec5 !important;
background-color: #cdd3d9;
border: 2px solid;
.nitro-catalog-navigation-section {
display: grid;
.nitro-catalog-navigation-section {
padding-left: 5px;
border-left: 2px solid #b6bec5;
}
}
.layout-grid-item {
font-size: $font-size-sm;
height: 23px !important;
border-color: unset !important;
background-color: #CDD3D9;
background-color: #cdd3d9;
border: 0 !important;
padding: 1px 3px;
@ -58,23 +67,21 @@
}
.nitro-catalog-layout-info-loyalty {
.info-loyalty-content {
background-repeat: no-repeat;
background-position: top right;
background-image: url('../../assets/images/catalog/diamond_info_illustration.gif');
padding-right:123px;
background-image: url("../../assets/images/catalog/diamond_info_illustration.gif");
padding-right: 123px;
}
.info-image {
width: 123px;
height:350px;
background-image: url('../../assets/images/catalog/diamond_info_illustration.gif');
height: 350px;
background-image: url("../../assets/images/catalog/diamond_info_illustration.gif");
}
}
.nitro-catalog-layout-vip-buy-grid {
.layout-grid-item {
height: 50px !important;
max-height: 50px !important;
@ -82,20 +89,18 @@
.icon-hc-banner {
width: 68px;
height: 40px;
background: url('../../assets/images/catalog/hc_big.png') center no-repeat;
background: url("../../assets/images/catalog/hc_big.png") center no-repeat;
}
}
}
.nitro-catalog-layout-marketplace-grid {
.layout-grid-item {
height: 75px !important;
}
}
.nitro-catalog-layout-vip-gifts-grid {
.layout-grid-item {
height: 55px !important;
max-height: 55px !important;
@ -108,8 +113,12 @@
}
.nitro-catalog-layout-bundle-grid {
.layout-grid-item {
background-color: transparent;
}
}
.nitro-catalog-header {
width: 290px;
height: 60px;
}

View File

@ -1,8 +1,9 @@
import { ILinkEventTracker } from '@nitrots/nitro-renderer';
import { FC, useEffect } from 'react';
import { AddEventLinkTracker, LocalizeText, RemoveLinkEventTracker } from '../../api';
import { Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
import { AddEventLinkTracker, GetConfiguration, LocalizeText, RemoveLinkEventTracker } from '../../api';
import { Column, Flex, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
import { useCatalog } from '../../hooks';
import { CatalogIconView } from './views/catalog-icon/CatalogIconView';
import { CatalogGiftView } from './views/gift/CatalogGiftView';
import { CatalogNavigationView } from './views/navigation/CatalogNavigationView';
import { GetCatalogLayout } from './views/page/layout/GetCatalogLayout';
@ -10,7 +11,7 @@ import { MarketplacePostOfferView } from './views/page/layout/marketplace/Market
export const CatalogView: FC<{}> = props =>
{
const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null } = useCatalog();
const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null, getNodeById } = useCatalog();
useEffect(() =>
{
@ -68,7 +69,7 @@ export const CatalogView: FC<{}> = props =>
return (
<>
{ isVisible &&
<NitroCardView uniqueKey="catalog" className="nitro-catalog">
<NitroCardView uniqueKey="catalog" className="nitro-catalog" style={ { width: GetConfiguration('catalog.headers') ? '710px' : '' } }>
<NitroCardHeaderView headerText={ LocalizeText('catalog.title') } onCloseClick={ event => setIsVisible(false) } />
<NitroCardTabsView>
{ rootNode && (rootNode.children.length > 0) && rootNode.children.map(child =>
@ -81,8 +82,11 @@ export const CatalogView: FC<{}> = props =>
if(searchResult) setSearchResult(null);
activateNode(child);
} }>
{ child.localization }
} } >
<Flex gap={ GetConfiguration('catalog.tab.icons') ? 1 : 0 } alignItems="center">
{ GetConfiguration('catalog.tab.icons') && <CatalogIconView icon={ child.iconId } /> }
{ child.localization }
</Flex>
</NitroCardTabsItemView>
);
}) }

View File

@ -0,0 +1,26 @@
import { FC, useEffect, useState } from 'react';
import { GetConfiguration } from '../../../../api';
import { Flex } from '../../../../common';
export interface CatalogHeaderViewProps
{
imageUrl?: string;
}
export const CatalogHeaderView: FC<CatalogHeaderViewProps> = props =>
{
const { imageUrl = null } = props;
const [ displayImageUrl, setDisplayImageUrl ] = useState('');
useEffect(() =>
{
setDisplayImageUrl(imageUrl ?? GetConfiguration<string>('catalog.asset.image.url').replace('%name%', 'catalog_header_roombuilder'));
}, [ imageUrl ]);
return <Flex center fullWidth className="nitro-catalog-header">
<img src={ displayImageUrl } onError={ ({ currentTarget }) =>
{
currentTarget.src = GetConfiguration<string>('catalog.asset.image.url').replace('%name%', 'catalog_header_roombuilder');
} } />
</Flex>;
}

View File

@ -1,7 +1,7 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { FC } from 'react';
import { ICatalogNode } from '../../../../api';
import { LayoutGridItem, Text } from '../../../../common';
import { Base, LayoutGridItem, Text } from '../../../../common';
import { useCatalog } from '../../../../hooks';
import { CatalogIconView } from '../catalog-icon/CatalogIconView';
import { CatalogNavigationSetView } from './CatalogNavigationSetView';
@ -9,23 +9,24 @@ import { CatalogNavigationSetView } from './CatalogNavigationSetView';
export interface CatalogNavigationItemViewProps
{
node: ICatalogNode;
child?: boolean;
}
export const CatalogNavigationItemView: FC<CatalogNavigationItemViewProps> = props =>
{
const { node = null } = props;
const { node = null, child = false } = props;
const { activateNode = null } = useCatalog();
return (
<>
<LayoutGridItem gap={ 1 } column={ false } itemActive={ node.isActive } onClick={ event => activateNode(node) }>
<Base className="nitro-catalog-navigation-section">
<LayoutGridItem gap={ 1 } column={ false } itemActive={ node.isActive } onClick={ event => activateNode(node) } className={ child ? 'inset' : '' }>
<CatalogIconView icon={ node.iconId } />
<Text grow truncate>{ node.localization }</Text>
{ node.isBranch &&
<FontAwesomeIcon icon={ node.isOpen ? 'caret-up' : 'caret-down' } /> }
</LayoutGridItem>
{ node.isOpen && node.isBranch &&
<CatalogNavigationSetView node={ node } /> }
</>
<CatalogNavigationSetView node={ node } child={ true } /> }
</Base>
);
}

View File

@ -5,11 +5,12 @@ import { CatalogNavigationItemView } from './CatalogNavigationItemView';
export interface CatalogNavigationSetViewProps
{
node: ICatalogNode;
child?: boolean;
}
export const CatalogNavigationSetView: FC<CatalogNavigationSetViewProps> = props =>
{
const { node = null } = props;
const { node = null, child = false } = props;
return (
<>
@ -17,7 +18,7 @@ export const CatalogNavigationSetView: FC<CatalogNavigationSetViewProps> = props
{
if(!n.isVisible) return null;
return <CatalogNavigationItemView key={ index } node={ n } />
return <CatalogNavigationItemView key={ index } node={ n } child={ child } />
}) }
</>
);

View File

@ -20,7 +20,7 @@ export const CatalogNavigationView: FC<CatalogNavigationViewProps> = props =>
<>
<CatalogSearchView />
<Column fullHeight className="nitro-catalog-navigation-grid-container rounded p-1" overflow="hidden">
<AutoGrid gap={ 1 } columnCount={ 1 }>
<AutoGrid id="nitro-catalog-main-navigation" gap={ 1 } columnCount={ 1 }>
{ searchResult && (searchResult.filteredNodes.length > 0) && searchResult.filteredNodes.map((n, index) =>
{
return <CatalogNavigationItemView key={ index } node={ n } />;

View File

@ -1,7 +1,8 @@
import { FC } from 'react';
import { ProductTypeEnum } from '../../../../../api';
import { Column, Flex, Grid, Text } from '../../../../../common';
import { GetConfiguration, ProductTypeEnum } from '../../../../../api';
import { Column, Flex, Grid, LayoutImage, Text } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
import { CatalogHeaderView } from '../../catalog-header/CatalogHeaderView';
import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView';
import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView';
import { CatalogLimitedItemWidgetView } from '../widgets/CatalogLimitedItemWidgetView';
@ -14,42 +15,47 @@ import { CatalogLayoutProps } from './CatalogLayout.types';
export const CatalogLayoutDefaultView: FC<CatalogLayoutProps> = props =>
{
const { page = null } = props;
const { currentOffer = null } = useCatalog();
const { currentOffer = null, currentPage = null } = useCatalog();
return (
<Grid>
<Column size={ 7 } overflow="hidden">
<CatalogItemGridWidgetView />
</Column>
<Column center={ !currentOffer } size={ 5 } overflow="hidden">
{ !currentOffer &&
<>
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</> }
{ currentOffer &&
<>
<Flex center overflow="hidden" style={ { height: 140 } }>
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE) &&
<>
<CatalogViewProductWidgetView />
<CatalogLimitedItemWidgetView fullWidth position="absolute" className="top-1" />
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-1 end-1" />
</> }
{ (currentOffer.product.productType === ProductTypeEnum.BADGE) && <CatalogAddOnBadgeWidgetView className="scale-2" /> }
</Flex>
<Column grow gap={ 1 }>
<Text grow truncate>{ currentOffer.localizationName }</Text>
<Flex justifyContent="between">
<Column gap={ 1 }>
<CatalogSpinnerWidgetView />
</Column>
<CatalogTotalPriceWidget justifyContent="end" alignItems="end" />
<>
<Grid>
<Column size={ 7 } overflow="hidden">
{ GetConfiguration('catalog.headers') &&
<CatalogHeaderView imageUrl={ currentPage.localization.getImage(0) }/> }
<CatalogItemGridWidgetView />
</Column>
<Column center={ !currentOffer } size={ 5 } overflow="hidden">
{ !currentOffer &&
<>
{ !!page.localization.getImage(1) &&
<LayoutImage imageUrl={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</> }
{ currentOffer &&
<>
<Flex center overflow="hidden" style={ { height: 140 } }>
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE) &&
<>
<CatalogViewProductWidgetView />
<CatalogLimitedItemWidgetView fullWidth position="absolute" className="top-1" />
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-1 end-1" />
</> }
{ (currentOffer.product.productType === ProductTypeEnum.BADGE) && <CatalogAddOnBadgeWidgetView className="scale-2" /> }
</Flex>
<CatalogPurchaseWidgetView />
</Column>
</> }
</Column>
</Grid>
<Column grow gap={ 1 }>
<Text grow truncate>{ currentOffer.localizationName }</Text>
<Flex justifyContent="between">
<Column gap={ 1 }>
<CatalogSpinnerWidgetView />
</Column>
<CatalogTotalPriceWidget justifyContent="end" alignItems="end" />
</Flex>
<CatalogPurchaseWidgetView />
</Column>
</> }
</Column>
</Grid>
</>
);
}

View File

@ -9,10 +9,22 @@ export const MarketplacePostOfferView : FC<{}> = props =>
{
const [ item, setItem ] = useState<FurnitureItem>(null);
const [ askingPrice, setAskingPrice ] = useState(0);
const [ tempAskingPrice, setTempAskingPrice ] = useState('0');
const { catalogOptions = null, setCatalogOptions = null } = useCatalog();
const { marketplaceConfiguration = null } = catalogOptions;
const { showConfirm = null } = useNotification();
const updateAskingPrice = (price: string) =>
{
setTempAskingPrice(price);
const newValue = parseInt(price);
if(isNaN(newValue) || (newValue === askingPrice)) return;
setAskingPrice(parseInt(price));
}
useMessageEvent<MarketplaceConfigurationEvent>(MarketplaceConfigurationEvent, event =>
{
const parser = event.getParser();
@ -64,7 +76,7 @@ export const MarketplacePostOfferView : FC<{}> = props =>
setItem(null)
}, null, null, LocalizeText('inventory.marketplace.confirm_offer.title'));
}
return (
<NitroCardView className="nitro-catalog-layout-marketplace-post-offer" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('inventory.marketplace.make_offer.title') } onCloseClick={ event => setItem(null) } />
@ -83,7 +95,7 @@ export const MarketplacePostOfferView : FC<{}> = props =>
{ LocalizeText('inventory.marketplace.make_offer.expiration_info', [ 'time' ], [ marketplaceConfiguration.offerTime.toString() ]) }
</Text>
<div className="input-group has-validation">
<input className="form-control form-control-sm" type="number" min={ 0 } value={ askingPrice } onChange={ event => setAskingPrice(parseInt(event.target.value)) } placeholder={ LocalizeText('inventory.marketplace.make_offer.price_request') } />
<input className="form-control form-control-sm" type="number" min={ 0 } value={ tempAskingPrice } onChange={ event => updateAskingPrice(event.target.value) } placeholder={ LocalizeText('inventory.marketplace.make_offer.price_request') } />
{ ((askingPrice < marketplaceConfiguration.minimumPrice) || isNaN(askingPrice)) &&
<Base className="invalid-feedback d-block">
{ LocalizeText('inventory.marketplace.make_offer.min_price', [ 'minprice' ], [ marketplaceConfiguration.minimumPrice.toString() ]) }

View File

@ -1,4 +1,4 @@
import { FC } from 'react';
import { FC, useEffect, useRef } from 'react';
import { AutoGrid, AutoGridProps, LayoutGridItem } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
@ -11,11 +11,17 @@ export const CatalogBundleGridWidgetView: FC<CatalogBundleGridWidgetViewProps> =
{
const { columnCount = 5, children = null, ...rest } = props;
const { currentOffer = null } = useCatalog();
const elementRef = useRef<HTMLDivElement>();
useEffect(() =>
{
if(elementRef && elementRef.current) elementRef.current.scrollTop = 0;
}, [ currentOffer ]);
if(!currentOffer) return null;
return (
<AutoGrid columnCount={ 5 } { ...rest }>
<AutoGrid innerRef={ elementRef } columnCount={ 5 } { ...rest }>
{ currentOffer.products && (currentOffer.products.length > 0) && currentOffer.products.map((product, index) => <LayoutGridItem key={ index } itemImage={ product.getIconUrl() } itemCount={ product.productCount } />) }
{ children }
</AutoGrid>

View File

@ -1,4 +1,4 @@
import { FC } from 'react';
import { FC, useEffect, useRef } from 'react';
import { IPurchasableOffer, ProductTypeEnum } from '../../../../../api';
import { AutoGrid, AutoGridProps } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
@ -13,6 +13,12 @@ export const CatalogItemGridWidgetView: FC<CatalogItemGridWidgetViewProps> = pro
{
const { columnCount = 5, children = null, ...rest } = props;
const { currentOffer = null, setCurrentOffer = null, currentPage = null, setPurchaseOptions = null } = useCatalog();
const elementRef = useRef<HTMLDivElement>();
useEffect(() =>
{
if(elementRef && elementRef.current) elementRef.current.scrollTop = 0;
}, [ currentPage ]);
if(!currentPage) return null;
@ -38,7 +44,7 @@ export const CatalogItemGridWidgetView: FC<CatalogItemGridWidgetViewProps> = pro
}
return (
<AutoGrid columnCount={ columnCount } { ...rest }>
<AutoGrid innerRef={ elementRef } columnCount={ columnCount } { ...rest }>
{ currentPage.offers && (currentPage.offers.length > 0) && currentPage.offers.map((offer, index) => <CatalogGridOfferView key={ index } itemActive={ (currentOffer && (currentOffer.offerId === offer.offerId)) } offer={ offer } selectOffer={ selectOffer } />) }
{ children }
</AutoGrid>

View File

@ -103,11 +103,7 @@ export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = pro
{
if(!currentOffer) return;
return () =>
{
setPurchaseState(CatalogPurchaseState.NONE);
setPurchaseOptions({ quantity: 1, extraData: null, extraParamRequired: false, previewStuffData: null });
}
setPurchaseState(CatalogPurchaseState.NONE);
}, [ currentOffer, setPurchaseOptions ]);
useEffect(() =>

View File

@ -1,4 +1,4 @@
import { FC, useEffect, useState } from 'react';
import { FC, useEffect, useRef, useState } from 'react';
import { IPurchasableOffer, LocalizeText, Offer, ProductTypeEnum } from '../../../../../api';
import { AutoGrid, AutoGridProps, Button, ButtonGroup } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
@ -18,6 +18,7 @@ export const CatalogSpacesWidgetView: FC<CatalogSpacesWidgetViewProps> = props =
const [ selectedGroupIndex, setSelectedGroupIndex ] = useState(-1);
const [ selectedOfferForGroup, setSelectedOfferForGroup ] = useState<IPurchasableOffer[]>(null);
const { currentPage = null, currentOffer = null, setCurrentOffer = null, setPurchaseOptions = null } = useCatalog();
const elementRef = useRef<HTMLDivElement>();
const setSelectedOffer = (offer: IPurchasableOffer) =>
{
@ -91,6 +92,11 @@ export const CatalogSpacesWidgetView: FC<CatalogSpacesWidgetViewProps> = props =
});
}, [ currentOffer, selectedGroupIndex, selectedOfferForGroup, setPurchaseOptions ]);
useEffect(() =>
{
if(elementRef && elementRef.current) elementRef.current.scrollTop = 0;
}, [ selectedGroupIndex ]);
if(!groupedOffers || (selectedGroupIndex === -1)) return null;
const offers = groupedOffers[selectedGroupIndex];
@ -100,7 +106,7 @@ export const CatalogSpacesWidgetView: FC<CatalogSpacesWidgetViewProps> = props =
<ButtonGroup>
{ SPACES_GROUP_NAMES.map((name, index) => <Button key={ index } active={ (selectedGroupIndex === index) } onClick={ event => setSelectedGroupIndex(index) }>{ LocalizeText(`catalog.spaces.tab.${ name }`) }</Button>) }
</ButtonGroup>
<AutoGrid columnCount={ columnCount } { ...rest }>
<AutoGrid innerRef={ elementRef } columnCount={ columnCount } { ...rest }>
{ offers && (offers.length > 0) && offers.map((offer, index) => <CatalogGridOfferView key={ index } itemActive={ (currentOffer && (currentOffer === offer)) } offer={ offer } selectOffer={ offer => setSelectedOffer(offer) } />) }
{ children }
</AutoGrid>

View File

@ -1,5 +1,5 @@
import { GuideSessionGetRequesterRoomMessageComposer, GuideSessionInviteRequesterMessageComposer, GuideSessionMessageMessageComposer, GuideSessionRequesterRoomMessageEvent, GuideSessionResolvedMessageComposer } from '@nitrots/nitro-renderer';
import { FC, KeyboardEvent, useCallback, useState } from 'react';
import { FC, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
import { GetSessionDataManager, GuideToolMessageGroup, LocalizeText, SendMessageComposer, TryVisitRoom } from '../../../api';
import { Base, Button, ButtonGroup, Column, Flex, LayoutAvatarImageView, Text } from '../../../common';
import { useMessageEvent } from '../../../hooks';
@ -16,10 +16,18 @@ interface GuideToolOngoingViewProps
export const GuideToolOngoingView: FC<GuideToolOngoingViewProps> = props =>
{
const scrollDiv = useRef<HTMLDivElement>(null);
const { isGuide = false, userId = 0, userName = null, userFigure = null, isTyping = false, messageGroups = [] } = props;
const [ messageText, setMessageText ] = useState<string>('');
useEffect(() =>
{
scrollDiv.current?.scrollIntoView({ block: 'end', behavior: 'smooth' });
}, [ messageGroups ]);
const visit = useCallback(() =>
{
SendMessageComposer(new GuideSessionGetRequesterRoomMessageComposer());
@ -38,7 +46,7 @@ export const GuideToolOngoingView: FC<GuideToolOngoingViewProps> = props =>
useMessageEvent<GuideSessionRequesterRoomMessageEvent>(GuideSessionRequesterRoomMessageEvent, event =>
{
const parser = event.getParser();
TryVisitRoom(parser.requesterRoomId);
});
@ -100,7 +108,8 @@ export const GuideToolOngoingView: FC<GuideToolOngoingViewProps> = props =>
</Base> }
</Flex>
);
}) }
}) }
<div ref={ scrollDiv } />
</Column>
</Column>
<Column gap={ 1 }>

View File

@ -70,8 +70,6 @@ export const HelpView: FC<{}> = props =>
setIsVisible(true);
}, [ activeReport ]);
if(!isVisible && !activeReport) return null;
const CurrentStepView = () =>
{
@ -97,19 +95,20 @@ export const HelpView: FC<{}> = props =>
return (
<>
<NitroCardView className="nitro-help" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('help.button.cfh') } onCloseClick={ onClose } />
<NitroCardContentView className="text-black">
<Grid>
<Column center size={ 5 } overflow="hidden">
<Base className="index-image" />
</Column>
<Column justifyContent="between" size={ 7 } overflow="hidden">
<CurrentStepView />
</Column>
</Grid>
</NitroCardContentView>
</NitroCardView>
{ isVisible &&
<NitroCardView className="nitro-help" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('help.button.cfh') } onCloseClick={ onClose } />
<NitroCardContentView className="text-black">
<Grid>
<Column center size={ 5 } overflow="hidden">
<Base className="index-image" />
</Column>
<Column justifyContent="between" size={ 7 } overflow="hidden">
<CurrentStepView />
</Column>
</Grid>
</NitroCardContentView>
</NitroCardView> }
<SanctionSatusView />
<NameChangeView />
</>

View File

@ -21,6 +21,7 @@ export const SelectReportedChatsView: FC<{}> = props =>
return messengerHistory.filter(chat => (chat.entityId === activeReport.reportedUserId) && (chat.type === ChatEntryType.TYPE_IM));
}
return [];
}, [ activeReport, chatHistory, messengerHistory ]);
const selectChat = (chatEntry: IChatEntry) =>
@ -62,7 +63,7 @@ export const SelectReportedChatsView: FC<{}> = props =>
<Text>{ LocalizeText('help.emergency.chat_report.description') }</Text>
</Column>
<Column gap={ 1 } overflow="hidden">
{ !!!userChats.length &&
{ !userChats || !userChats.length &&
<Text>{ LocalizeText('help.cfh.error.no_user_data') }</Text> }
{ (userChats.length > 0) &&
<AutoGrid gap={ 1 } columnCount={ 1 } columnMinHeight={ 25 } overflow="auto">

View File

@ -17,13 +17,11 @@ export const NameChangeView:FC<{}> = props =>
const [ layout, setLayout ] = useState<string>(INIT);
const [ newUsername, setNewUsername ] = useState<string>('');
const onHelpNameChangeEvent = useCallback((event: HelpNameChangeEvent) =>
useUiEvent<HelpNameChangeEvent>(HelpNameChangeEvent.INIT, event =>
{
setLayout(INIT);
setIsVisible(true);
}, []);
useUiEvent(HelpNameChangeEvent.INIT, onHelpNameChangeEvent);
});
const onAction = useCallback((action: string, value?: string) =>
{

View File

@ -6,14 +6,14 @@ import { useInventoryBadges, useInventoryUnseenTracker } from '../../../../hooks
export const InventoryBadgeItemView: FC<PropsWithChildren<{ badgeCode: string }>> = props =>
{
const { badgeCode = null, children = null, ...rest } = props;
const { selectedBadgeCode = null, setSelectedBadgeCode = null, getBadgeId = null } = useInventoryBadges();
const { selectedBadgeCode = null, setSelectedBadgeCode = null, toggleBadge = null, getBadgeId = null } = useInventoryBadges();
const { isUnseen = null } = useInventoryUnseenTracker();
const unseen = isUnseen(UnseenItemCategory.BADGE, getBadgeId(badgeCode));
return (
<LayoutGridItem itemActive={ (selectedBadgeCode === badgeCode) } itemUnseen={ unseen } onMouseDown={ event => setSelectedBadgeCode(badgeCode) } { ...rest }>
<LayoutGridItem itemActive={ (selectedBadgeCode === badgeCode) } itemUnseen={ unseen } onMouseDown={ event => setSelectedBadgeCode(badgeCode) } onDoubleClick={ event => toggleBadge(selectedBadgeCode) } { ...rest }>
<LayoutBadgeImageView badgeCode={ badgeCode } />
{ children }
</LayoutGridItem>
);
}
}

View File

@ -26,13 +26,16 @@ export const InventoryBotItemView: FC<PropsWithChildren<{ botItem: IBotItem }>>
case MouseEventType.ROLL_OUT:
if(!isMouseDown || (selectedBot !== botItem)) return;
attemptBotPlacement(botItem);
return;
case 'dblclick':
attemptBotPlacement(botItem);
return;
}
}
return (
<LayoutGridItem itemActive={ (selectedBot === botItem) } itemUnseen={ unseen } onMouseDown={ onMouseEvent } onMouseUp={ onMouseEvent } onMouseOut={ onMouseEvent } { ...rest }>
<LayoutGridItem itemActive={ (selectedBot === botItem) } itemUnseen={ unseen } onMouseDown={ onMouseEvent } onMouseUp={ onMouseEvent } onMouseOut={ onMouseEvent } onDoubleClick={ onMouseEvent } { ...rest }>
<LayoutAvatarImageView figure={ botItem.botData.figure } direction={ 3 } headOnly={ true } />
{ children }
</LayoutGridItem>

View File

@ -24,6 +24,9 @@ export const InventoryFurnitureItemView: FC<{ groupItem: GroupItem }> = props =>
case MouseEventType.ROLL_OUT:
if(!isMouseDown || !(groupItem === selectedItem)) return;
attemptItemPlacement(groupItem);
return;
case 'dblclick':
attemptItemPlacement(groupItem);
return;
}
@ -31,5 +34,5 @@ export const InventoryFurnitureItemView: FC<{ groupItem: GroupItem }> = props =>
const count = groupItem.getUnlockedCount();
return <LayoutGridItem className={ !count ? 'opacity-0-5 ' : '' } itemImage={ groupItem.iconUrl } itemCount={ groupItem.getUnlockedCount() } itemActive={ (groupItem === selectedItem) } itemUniqueNumber={ groupItem.stuffData.uniqueNumber } itemUnseen={ groupItem.hasUnseenItems } onMouseDown={ onMouseEvent } onMouseUp={ onMouseEvent } onMouseOut={ onMouseEvent } { ...rest } />;
return <LayoutGridItem className={ !count ? 'opacity-0-5 ' : '' } itemImage={ groupItem.iconUrl } itemCount={ groupItem.getUnlockedCount() } itemActive={ (groupItem === selectedItem) } itemUniqueNumber={ groupItem.stuffData.uniqueNumber } itemUnseen={ groupItem.hasUnseenItems } onMouseDown={ onMouseEvent } onMouseUp={ onMouseEvent } onMouseOut={ onMouseEvent } onDoubleClick={ onMouseEvent } { ...rest } />;
}

View File

@ -21,6 +21,7 @@ export const InventoryTradeView: FC<InventoryTradeViewProps> = props =>
const [ otherGroupItem, setOtherGroupItem ] = useState<GroupItem>(null);
const [ filteredGroupItems, setFilteredGroupItems ] = useState<GroupItem[]>(null);
const [ countdownTick, setCountdownTick ] = useState(3);
const [ quantity, setQuantity ] = useState<number>(1);
const { ownUser = null, otherUser = null, groupItems = [], tradeState = TradeState.TRADING_STATE_READY, progressTrade = null, removeItem = null, setTradeState = null } = useInventoryTrade();
const { simpleAlert = null } = useNotification();
@ -118,6 +119,29 @@ export const InventoryTradeView: FC<InventoryTradeViewProps> = props =>
return <FontAwesomeIcon icon={ iconName } className={ 'text-' + textColor } />
}
const updateQuantity = (value: number, totalItemCount: number) =>
{
if(isNaN(Number(value)) || Number(value) < 0 || !value) value = 1;
value = Math.max(Number(value), 1);
value = Math.min(Number(value), totalItemCount);
if(value === quantity) return;
setQuantity(value);
}
const changeCount = (totalItemCount: number) =>
{
updateQuantity(quantity, totalItemCount);
attemptItemOffer(quantity);
}
useEffect(() =>
{
setQuantity(1);
}, [ groupItem ]);
useEffect(() =>
{
if(tradeState !== TradeState.TRADING_STATE_COUNTDOWN) return;
@ -159,18 +183,29 @@ export const InventoryTradeView: FC<InventoryTradeViewProps> = props =>
const count = item.getUnlockedCount();
return (
<LayoutGridItem key={ index } className={ !count ? 'opacity-0-5 ' : '' } itemImage={ item.iconUrl } itemCount={ count } itemActive={ (groupItem === item) } itemUniqueNumber={ item.stuffData.uniqueNumber } onClick={ event => (count && setGroupItem(item)) }>
<LayoutGridItem key={ index } className={ !count ? 'opacity-0-5 ' : '' } itemImage={ item.iconUrl } itemCount={ count } itemActive={ (groupItem === item) } itemUniqueNumber={ item.stuffData.uniqueNumber } onClick={ event => (count && setGroupItem(item)) } onDoubleClick={ event => attemptItemOffer(1) }>
{ ((count > 0) && (groupItem === item)) &&
<Button position="absolute" variant="success" className="trade-button bottom-1 end-1" onClick={ event => attemptItemOffer(1) }>
<FontAwesomeIcon icon="chevron-right" />
</Button> }
<Button position="absolute" variant="success" className="trade-button bottom-1 end-1" onClick={ event => attemptItemOffer(1) }>
<FontAwesomeIcon icon="chevron-right" />
</Button>
}
</LayoutGridItem>
);
}) }
</AutoGrid>
<Base fullWidth className="badge bg-muted">
{ groupItem ? groupItem.name : LocalizeText('catalog_selectproduct') }
</Base>
<Column gap={ 1 } alignItems="end">
<Grid overflow="hidden">
<Column size={ 6 } overflow="hidden">
<input type="number" className="form-control form-control-sm quantity-input" placeholder={ LocalizeText('catalog.bundlewidget.spinner.select.amount') } disabled={ !groupItem } value={ quantity } onChange={ event => setQuantity(event.target.valueAsNumber) } />
</Column>
<Column size={ 6 } overflow="hidden">
<Button variant="secondary" disabled={ !groupItem } onClick={ event => changeCount(groupItem.getUnlockedCount()) }>{ LocalizeText('inventory.trading.areoffering') }</Button>
</Column>
</Grid>
<Base fullWidth className="badge bg-muted">
{ groupItem ? groupItem.name : LocalizeText('catalog_selectproduct') }
</Base>
</Column>
</Flex>
</Column>
<Column size={ 8 } overflow="hidden">
@ -188,11 +223,11 @@ export const InventoryTradeView: FC<InventoryTradeViewProps> = props =>
if(!item) return <LayoutGridItem key={ i } />;
return (
<LayoutGridItem key={ i } itemActive={ (ownGroupItem === item) } itemImage={ item.iconUrl } itemCount={ item.getTotalCount() } itemUniqueNumber={ item.stuffData.uniqueNumber } onClick={ event => setOwnGroupItem(item) }>
<LayoutGridItem key={ i } itemActive={ (ownGroupItem === item) } itemImage={ item.iconUrl } itemCount={ item.getTotalCount() } itemUniqueNumber={ item.stuffData.uniqueNumber } onClick={ event => setOwnGroupItem(item) } onDoubleClick={ event => removeItem(item) }>
{ (ownGroupItem === item) &&
<Button position="absolute" variant="danger" className="trade-button bottom-1 start-1" onClick={ event => removeItem(item) }>
<FontAwesomeIcon icon="chevron-left" />
</Button> }
<Button position="absolute" variant="danger" className="trade-button bottom-1 start-1" onClick={ event => removeItem(item) }>
<FontAwesomeIcon icon="chevron-left" />
</Button> }
</LayoutGridItem>
);
}) }

View File

@ -26,13 +26,16 @@ export const InventoryPetItemView: FC<PropsWithChildren<{ petItem: IPetItem }>>
case MouseEventType.ROLL_OUT:
if(!isMouseDown || !(petItem === selectedPet)) return;
attemptPetPlacement(petItem);
return;
case 'dblclick':
attemptPetPlacement(petItem);
return;
}
}
return (
<LayoutGridItem itemActive={ (petItem === selectedPet) } itemUnseen={ unseen } onMouseDown={ onMouseEvent } onMouseUp={ onMouseEvent } onMouseOut={ onMouseEvent } { ...rest }>
<LayoutGridItem itemActive={ (petItem === selectedPet) } itemUnseen={ unseen } onMouseDown={ onMouseEvent } onMouseUp={ onMouseEvent } onMouseOut={ onMouseEvent } onDoubleClick={ onMouseEvent } { ...rest }>
<LayoutPetImageView figure={ petItem.petData.figureData.figuredata } direction={ 3 } headOnly={ true } />
{ children }
</LayoutGridItem>

View File

@ -24,6 +24,7 @@ export const NavigatorView: FC<{}> = props =>
const [ needsSearch, setNeedsSearch ] = useState(false);
const { searchResult = null, topLevelContext = null, topLevelContexts = null, navigatorData = null } = useNavigator();
const pendingSearch = useRef<{ value: string, code: string }>(null);
const elementRef = useRef<HTMLDivElement>();
useRoomSessionManagerEvent<RoomSessionEvent>(RoomSessionEvent.CREATED, event =>
{
@ -158,6 +159,8 @@ export const NavigatorView: FC<{}> = props =>
if(!searchResult) return;
setIsLoading(false);
if(elementRef && elementRef.current) elementRef.current.scrollTop = 0;
}, [ searchResult ]);
useEffect(() =>
@ -214,7 +217,7 @@ export const NavigatorView: FC<{}> = props =>
{ !isCreatorOpen &&
<>
<NavigatorSearchView sendSearch={ sendSearch } />
<Column overflow="auto">
<Column innerRef={ elementRef } overflow="auto">
{ (searchResult && searchResult.results.map((result, index) => <NavigatorSearchResultView key={ index } searchResult={ result } />)) }
</Column>
</> }

View File

@ -13,7 +13,6 @@ export interface NavigatorSearchResultViewProps extends AutoGridProps
export const NavigatorSearchResultView: FC<NavigatorSearchResultViewProps> = props =>
{
const { searchResult = null, ...rest } = props;
const [ isExtended, setIsExtended ] = useState(true);
const [ displayMode, setDisplayMode ] = useState<number>(0);
@ -44,7 +43,7 @@ export const NavigatorSearchResultView: FC<NavigatorSearchResultViewProps> = pro
//setIsExtended(searchResult.closed);
setDisplayMode(searchResult.mode);
}, [ searchResult,props ]);
}, [ searchResult ]);
const gridHasTwoColumns = (displayMode >= NavigatorSearchResultViewDisplayMode.THUMBNAILS);

View File

@ -70,6 +70,17 @@
}
}
.body-image-plant {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
max-width: 68px;
height: 85px;
max-height: 90px;
border-radius: $border-radius;
}
.badge-image {
width: 45px;
height: 45px;

View File

@ -1,7 +1,9 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { FC } from 'react';
import { AvatarInfoPet, LocalizeText } from '../../../../../api';
import { Base, Column, Flex, LayoutPetImageView, Text, UserProfileIconView } from '../../../../../common';
import { PetRespectComposer, PetType } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { AvatarInfoPet, ConvertSeconds, CreateLinkEvent, GetConfiguration, LocalizeText, SendMessageComposer } from '../../../../../api';
import { Base, Button, Column, Flex, LayoutCounterTimeView, LayoutPetImageView, LayoutRarityLevelView, Text, UserProfileIconView } from '../../../../../common';
import { useRoom, useSessionInfo } from '../../../../../hooks';
interface InfoStandWidgetPetViewProps
{
@ -12,72 +14,196 @@ interface InfoStandWidgetPetViewProps
export const InfoStandWidgetPetView: FC<InfoStandWidgetPetViewProps> = props =>
{
const { avatarInfo = null, onClose = null } = props;
const [ remainingGrowTime, setRemainingGrowTime ] = useState(0);
const [ remainingTimeToLive, setRemainingTimeToLive ] = useState(0);
const { roomSession = null } = useRoom();
const { petRespectRemaining = 0, respectPet = null } = useSessionInfo();
useEffect(() =>
{
setRemainingGrowTime(avatarInfo.remainingGrowTime);
setRemainingTimeToLive(avatarInfo.remainingTimeToLive);
}, [ avatarInfo ]);
useEffect(() =>
{
if((avatarInfo.petType !== PetType.MONSTERPLANT) || avatarInfo.dead) return;
const interval = setInterval(() =>
{
setRemainingGrowTime(prevValue => (prevValue - 1));
setRemainingTimeToLive(prevValue => (prevValue - 1));
}, 1000);
return () => clearInterval(interval);
}, [ avatarInfo ]);
if(!avatarInfo) return null;
const processButtonAction = (action: string) =>
{
let hideMenu = true;
if (!action || action == '') return;
switch (action)
{
case 'respect':
respectPet(avatarInfo.id);
if((petRespectRemaining - 1) >= 1) hideMenu = false;
break;
case 'buyfood':
CreateLinkEvent('catalog/open/' + GetConfiguration('catalog.links')['pets.buy_saddle']);
break;
case 'train':
// not coded
break;
case 'treat':
SendMessageComposer(new PetRespectComposer(avatarInfo.id));
break;
case 'compost':
roomSession?.compostPlant(avatarInfo.id);
break;
case 'pick_up':
roomSession?.pickupPet(avatarInfo.id);
break;
}
if(hideMenu) onClose();
}
return (
<Column className="nitro-infostand rounded">
<Column overflow="visible" className="container-fluid content-area" gap={ 1 }>
<Column gap={ 1 }>
<Flex alignItems="center" justifyContent="between" gap={ 1 }>
<Text variant="white" small wrap>{ avatarInfo.name }</Text>
<FontAwesomeIcon icon="times" className="cursor-pointer" onClick={ onClose } />
</Flex>
<Text variant="white" small wrap>{ LocalizeText(`pet.breed.${ avatarInfo.petType }.${ avatarInfo.petBreed }`) }</Text>
<hr className="m-0" />
</Column>
<Column gap={ 1 }>
<Flex gap={ 1 }>
<Column fullWidth overflow="hidden" className="body-image pet p-1">
<LayoutPetImageView figure={ avatarInfo.petFigure } posture={ avatarInfo.posture } direction={ 4 } />
</Column>
<Column grow gap={ 1 }>
<Text variant="white" center small wrap>{ LocalizeText('pet.level', [ 'level', 'maxlevel' ], [ avatarInfo.level.toString(), avatarInfo.maximumLevel.toString() ]) }</Text>
<Column alignItems="center" gap={ 1 }>
<Text variant="white" small truncate>{ LocalizeText('infostand.pet.text.happiness') }</Text>
<Base fullWidth overflow="hidden" position="relative" className="bg-light-dark rounded">
<Flex fit center position="absolute">
<Text variant="white" small>{ avatarInfo.happyness + '/' + avatarInfo.maximumHappyness }</Text>
</Flex>
<Base className="bg-info rounded pet-stats" style={ { width: (avatarInfo.happyness / avatarInfo.maximumHappyness) * 100 + '%' } } />
</Base>
<Column gap={ 1 } alignItems="end">
<Column className="nitro-infostand rounded">
<Column overflow="visible" className="container-fluid content-area" gap={ 1 }>
<Column gap={ 1 }>
<Flex alignItems="center" justifyContent="between" gap={ 1 }>
<Text variant="white" small wrap>{ avatarInfo.name }</Text>
<FontAwesomeIcon icon="times" className="cursor-pointer" onClick={ onClose } />
</Flex>
<Text variant="white" small wrap>{ LocalizeText(`pet.breed.${ avatarInfo.petType }.${ avatarInfo.petBreed }`) }</Text>
<hr className="m-0" />
</Column>
{ (avatarInfo.petType === PetType.MONSTERPLANT) &&
<>
<Column center gap={ 1 }>
<LayoutPetImageView figure={ avatarInfo.petFigure } posture={ avatarInfo.posture } direction={ 4 } />
<hr className="m-0" />
</Column>
<Column alignItems="center" gap={ 1 }>
<Text variant="white" small truncate>{ LocalizeText('infostand.pet.text.experience') }</Text>
<Base fullWidth overflow="hidden" position="relative" className="bg-light-dark rounded">
<Flex fit center position="absolute">
<Text variant="white" small>{ avatarInfo.experience + '/' + avatarInfo.levelExperienceGoal }</Text>
</Flex>
<Base className="bg-purple rounded pet-stats" style={ { width: (avatarInfo.experience / avatarInfo.levelExperienceGoal) * 100 + '%' } } />
</Base>
<Column gap={ 2 }>
{ !avatarInfo.dead &&
<Column alignItems="center" gap={ 1 }>
<Text variant="white" center small wrap>{ LocalizeText('pet.level', [ 'level', 'maxlevel' ], [ avatarInfo.level.toString(), avatarInfo.maximumLevel.toString() ]) }</Text>
</Column> }
<Column alignItems="center" gap={ 1 }>
<Text variant="white" small truncate>{ LocalizeText('infostand.pet.text.wellbeing') }</Text>
<Base fullWidth overflow="hidden" position="relative" className="bg-light-dark rounded">
<Flex fit center position="absolute">
<Text variant="white" small>{ avatarInfo.dead ? '00:00:00' : ConvertSeconds((remainingTimeToLive == 0 ? avatarInfo.remainingTimeToLive : remainingTimeToLive)).split(':')[1] + ':' + ConvertSeconds((remainingTimeToLive == null || remainingTimeToLive == undefined ? 0 : remainingTimeToLive)).split(':')[2] + ':' + ConvertSeconds((remainingTimeToLive == null || remainingTimeToLive == undefined ? 0 : remainingTimeToLive)).split(':')[3] }</Text>
</Flex>
<Base className="bg-success rounded pet-stats" style={ { width: avatarInfo.dead ? '0' : Math.round((avatarInfo.maximumTimeToLive * 100) / (remainingTimeToLive)).toString() } } />
</Base>
</Column>
{ remainingGrowTime != 0 && remainingGrowTime > 0 &&
<Column alignItems="center" gap={ 1 }>
<Text variant="white" small truncate>{ LocalizeText('infostand.pet.text.growth') }</Text>
<LayoutCounterTimeView className="top-2 end-2" day={ ConvertSeconds(remainingGrowTime).split(':')[0] } hour={ ConvertSeconds(remainingGrowTime).split(':')[1] } minutes={ ConvertSeconds(remainingGrowTime).split(':')[2] } seconds={ ConvertSeconds(remainingGrowTime).split(':')[3] } />
</Column> }
<Column alignItems="center" gap={ 1 }>
<Text variant="white" small truncate>{ LocalizeText('infostand.pet.text.raritylevel', [ 'level' ], [ LocalizeText(`infostand.pet.raritylevel.${ avatarInfo.rarityLevel }`) ]) }</Text>
<LayoutRarityLevelView className="top-2 end-2" level={ avatarInfo.rarityLevel } />
</Column>
<hr className="m-0" />
</Column>
<Column alignItems="center" gap={ 1 }>
<Text variant="white" small truncate>{ LocalizeText('infostand.pet.text.energy') }</Text>
<Base fullWidth overflow="hidden" position="relative" className="bg-light-dark rounded">
<Flex fit center position="absolute">
<Text variant="white" small>{ avatarInfo.energy + '/' + avatarInfo.maximumEnergy }</Text>
</Flex>
<Base className="bg-success rounded pet-stats" style={ { width: (avatarInfo.energy / avatarInfo.maximumEnergy) * 100 + '%' } } />
</Base>
<Column gap={ 1 }>
<Text variant="white" small wrap>{ LocalizeText('pet.age', [ 'age' ], [ avatarInfo.age.toString() ]) }</Text>
<hr className="m-0" />
</Column>
</Column>
</Flex>
<hr className="m-0" />
</Column>
<Column gap={ 1 }>
<Text variant="white" small wrap>{ LocalizeText('infostand.text.petrespect', [ 'count' ], [ avatarInfo.respect.toString() ]) }</Text>
<Text variant="white" small wrap>{ LocalizeText('pet.age', [ 'age' ], [ avatarInfo.age.toString() ]) }</Text>
<hr className="m-0" />
</Column>
<Column gap={ 1 }>
<Flex alignItems="center" gap={ 1 }>
<UserProfileIconView userId={ avatarInfo.ownerId } />
<Text variant="white" small wrap>
{ LocalizeText('infostand.text.petowner', [ 'name' ], [ avatarInfo.ownerName ]) }
</Text>
</Flex>
</> }
{ (avatarInfo.petType !== PetType.MONSTERPLANT) &&
<>
<Column gap={ 1 }>
<Flex gap={ 1 }>
<Column fullWidth overflow="hidden" className="body-image pet p-1">
<LayoutPetImageView figure={ avatarInfo.petFigure } posture={ avatarInfo.posture } direction={ 4 } />
</Column>
<Column grow gap={ 1 }>
<Text variant="white" center small wrap>{ LocalizeText('pet.level', [ 'level', 'maxlevel' ], [ avatarInfo.level.toString(), avatarInfo.maximumLevel.toString() ]) }</Text>
<Column alignItems="center" gap={ 1 }>
<Text variant="white" small truncate>{ LocalizeText('infostand.pet.text.happiness') }</Text>
<Base fullWidth overflow="hidden" position="relative" className="bg-light-dark rounded">
<Flex fit center position="absolute">
<Text variant="white" small>{ avatarInfo.happyness + '/' + avatarInfo.maximumHappyness }</Text>
</Flex>
<Base className="bg-info rounded pet-stats" style={ { width: (avatarInfo.happyness / avatarInfo.maximumHappyness) * 100 + '%' } } />
</Base>
</Column>
<Column alignItems="center" gap={ 1 }>
<Text variant="white" small truncate>{ LocalizeText('infostand.pet.text.experience') }</Text>
<Base fullWidth overflow="hidden" position="relative" className="bg-light-dark rounded">
<Flex fit center position="absolute">
<Text variant="white" small>{ avatarInfo.experience + '/' + avatarInfo.levelExperienceGoal }</Text>
</Flex>
<Base className="bg-purple rounded pet-stats" style={ { width: (avatarInfo.experience / avatarInfo.levelExperienceGoal) * 100 + '%' } } />
</Base>
</Column>
<Column alignItems="center" gap={ 1 }>
<Text variant="white" small truncate>{ LocalizeText('infostand.pet.text.energy') }</Text>
<Base fullWidth overflow="hidden" position="relative" className="bg-light-dark rounded">
<Flex fit center position="absolute">
<Text variant="white" small>{ avatarInfo.energy + '/' + avatarInfo.maximumEnergy }</Text>
</Flex>
<Base className="bg-success rounded pet-stats" style={ { width: (avatarInfo.energy / avatarInfo.maximumEnergy) * 100 + '%' } } />
</Base>
</Column>
</Column>
</Flex>
<hr className="m-0" />
</Column>
<Column gap={ 1 }>
{ (avatarInfo.petType !== PetType.MONSTERPLANT) &&
<Text variant="white" small wrap>{ LocalizeText('infostand.text.petrespect', [ 'count' ], [ avatarInfo.respect.toString() ]) }</Text> }
<Text variant="white" small wrap>{ LocalizeText('pet.age', [ 'age' ], [ avatarInfo.age.toString() ]) }</Text>
<hr className="m-0" />
</Column>
</> }
<Column gap={ 1 }>
<Flex alignItems="center" gap={ 1 }>
<UserProfileIconView userId={ avatarInfo.ownerId } />
<Text variant="white" small wrap>
{ LocalizeText('infostand.text.petowner', [ 'name' ], [ avatarInfo.ownerName ]) }
</Text>
</Flex>
</Column>
</Column>
</Column>
<Flex gap={ 1 } justifyContent="end">
{ (avatarInfo.petType !== PetType.MONSTERPLANT) &&
<Button variant="dark" onClick={ event => processButtonAction('buyfood') }>
{ LocalizeText('infostand.button.buyfood') }
</Button> }
{ avatarInfo.isOwner && (avatarInfo.petType !== PetType.MONSTERPLANT) &&
<Button variant="dark" onClick={ event => processButtonAction('train') }>
{ LocalizeText('infostand.button.train') }
</Button> }
{ !avatarInfo.dead && ((avatarInfo.energy / avatarInfo.maximumEnergy) < 0.98) && (avatarInfo.petType === PetType.MONSTERPLANT) &&
<Button variant="dark" onClick={ event => processButtonAction('treat') }>
{ LocalizeText('infostand.button.pettreat') }
</Button> }
{ roomSession?.isRoomOwner && (avatarInfo.petType === PetType.MONSTERPLANT) &&
<Button variant="dark" onClick={ event => processButtonAction('compost') }>
{ LocalizeText('infostand.button.compost') }
</Button> }
{ avatarInfo.isOwner &&
<Button variant="dark" onClick={ event => processButtonAction('pick_up') }>
{ LocalizeText('inventory.pets.pickup') }
</Button> }
{ (petRespectRemaining > 0) && (avatarInfo.petType !== PetType.MONSTERPLANT) &&
<Button variant="dark" onClick={ event => processButtonAction('respect') }>
{ LocalizeText('infostand.button.petrespect', [ 'count' ], [ petRespectRemaining.toString() ]) }
</Button> }
</Flex>
</Column>
);
}

View File

@ -3,7 +3,7 @@ import { RoomControllerLevel, RoomObjectCategory, RoomObjectVariable, RoomUnitGi
import { FC, useEffect, useMemo, useState } from 'react';
import { AvatarInfoUser, CreateLinkEvent, DispatchUiEvent, GetOwnRoomObject, GetSessionDataManager, GetUserProfile, LocalizeText, MessengerFriend, ReportType, RoomWidgetUpdateChatInputContentEvent, SendMessageComposer } from '../../../../../api';
import { Base, Flex } from '../../../../../common';
import { useFriends, useHelp, useRoom } from '../../../../../hooks';
import { useFriends, useHelp, useRoom, useSessionInfo } from '../../../../../hooks';
import { ContextMenuHeaderView } from '../../context-menu/ContextMenuHeaderView';
import { ContextMenuListItemView } from '../../context-menu/ContextMenuListItemView';
import { ContextMenuView } from '../../context-menu/ContextMenuView';
@ -26,10 +26,10 @@ export const AvatarInfoWidgetAvatarView: FC<AvatarInfoWidgetAvatarViewProps> = p
{
const { avatarInfo = null, onClose = null } = props;
const [ mode, setMode ] = useState(MODE_NORMAL);
const [ respectsLeft, setRespectsLeft ] = useState(0);
const { canRequestFriend = null } = useFriends();
const { report = null } = useHelp();
const { roomSession = null } = useRoom();
const { userRespectRemaining = 0, respectUser = null } = useSessionInfo();
const isShowGiveRights = useMemo(() =>
{
@ -113,13 +113,9 @@ export const AvatarInfoWidgetAvatarView: FC<AvatarInfoWidgetAvatarViewProps> = p
setMode(MODE_RELATIONSHIP);
break;
case 'respect': {
let newRespectsLeft = (respectsLeft - 1);
setRespectsLeft(newRespectsLeft);
respectUser(avatarInfo.webID);
GetSessionDataManager().giveRespect(avatarInfo.webID);
if(newRespectsLeft > 0) hideMenu = false;
if((userRespectRemaining - 1) >= 1) hideMenu = false;
break;
}
case 'ignore':
@ -203,7 +199,6 @@ export const AvatarInfoWidgetAvatarView: FC<AvatarInfoWidgetAvatarViewProps> = p
useEffect(() =>
{
setMode(MODE_NORMAL);
setRespectsLeft(avatarInfo.respectLeft);
}, [ avatarInfo ]);
return (
@ -223,9 +218,9 @@ export const AvatarInfoWidgetAvatarView: FC<AvatarInfoWidgetAvatarViewProps> = p
<ContextMenuListItemView onClick={ event => processAction('whisper') }>
{ LocalizeText('infostand.button.whisper') }
</ContextMenuListItemView>
{ (respectsLeft > 0) &&
{ (userRespectRemaining > 0) &&
<ContextMenuListItemView onClick={ event => processAction('respect') }>
{ LocalizeText('infostand.button.respect', [ 'count' ], [ respectsLeft.toString() ]) }
{ LocalizeText('infostand.button.respect', [ 'count' ], [ userRespectRemaining.toString() ]) }
</ContextMenuListItemView> }
{ !canRequestFriend(avatarInfo.webID) &&
<ContextMenuListItemView onClick={ event => processAction('relationship') }>

View File

@ -1,7 +1,7 @@
import { PetRespectComposer, PetType, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomUnitGiveHandItemPetComposer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useState } from 'react';
import { AvatarInfoPet, CreateLinkEvent, GetConfiguration, GetOwnRoomObject, GetSessionDataManager, LocalizeText, SendMessageComposer } from '../../../../../api';
import { useRoom } from '../../../../../hooks';
import { AvatarInfoPet, CreateLinkEvent, GetConfiguration, GetOwnRoomObject, LocalizeText, SendMessageComposer } from '../../../../../api';
import { useRoom, useSessionInfo } from '../../../../../hooks';
import { ContextMenuHeaderView } from '../../context-menu/ContextMenuHeaderView';
import { ContextMenuListItemView } from '../../context-menu/ContextMenuListItemView';
import { ContextMenuView } from '../../context-menu/ContextMenuView';
@ -21,8 +21,8 @@ export const AvatarInfoWidgetOwnPetView: FC<AvatarInfoWidgetOwnPetViewProps> = p
{
const { avatarInfo = null, onClose = null } = props;
const [ mode, setMode ] = useState(MODE_NORMAL);
const [ respectsLeft, setRespectsLeft ] = useState(0);
const { roomSession = null } = useRoom();
const { petRespectRemaining = 0, respectPet = null } = useSessionInfo();
const canGiveHandItem = useMemo(() =>
{
@ -49,18 +49,9 @@ export const AvatarInfoWidgetOwnPetView: FC<AvatarInfoWidgetOwnPetViewProps> = p
switch(name)
{
case 'respect':
let newRespectsLeft = 0;
respectPet(avatarInfo.id);
setRespectsLeft(prevValue =>
{
newRespectsLeft = (prevValue - 1);
return newRespectsLeft;
});
GetSessionDataManager().givePetRespect(avatarInfo.id);
if(newRespectsLeft > 0) hideMenu = false;
if((petRespectRemaining - 1) >= 1) hideMenu = false;
break;
case 'treat':
SendMessageComposer(new PetRespectComposer(avatarInfo.id));
@ -131,8 +122,6 @@ export const AvatarInfoWidgetOwnPetView: FC<AvatarInfoWidgetOwnPetViewProps> = p
return MODE_NORMAL;
});
setRespectsLeft(avatarInfo.respectsPetLeft);
}, [ avatarInfo ]);
return (
@ -142,9 +131,9 @@ export const AvatarInfoWidgetOwnPetView: FC<AvatarInfoWidgetOwnPetViewProps> = p
</ContextMenuHeaderView>
{ (mode === MODE_NORMAL) &&
<>
{ (respectsLeft > 0) &&
{ (petRespectRemaining > 0) &&
<ContextMenuListItemView onClick={ event => processAction('respect') }>
{ LocalizeText('infostand.button.petrespect', [ 'count' ], [ respectsLeft.toString() ]) }
{ LocalizeText('infostand.button.petrespect', [ 'count' ], [ petRespectRemaining.toString() ]) }
</ContextMenuListItemView> }
<ContextMenuListItemView onClick={ event => processAction('train') }>
{ LocalizeText('infostand.button.train') }
@ -170,9 +159,9 @@ export const AvatarInfoWidgetOwnPetView: FC<AvatarInfoWidgetOwnPetViewProps> = p
<input type="checkbox" checked={ !!avatarInfo.publiclyRideable } readOnly={ true } />
{ LocalizeText('infostand.button.toggle_riding_permission') }
</ContextMenuListItemView>
{ (respectsLeft > 0) &&
{ (petRespectRemaining > 0) &&
<ContextMenuListItemView onClick={ event => processAction('respect') }>
{ LocalizeText('infostand.button.petrespect', [ 'count' ], [ respectsLeft.toString() ]) }
{ LocalizeText('infostand.button.petrespect', [ 'count' ], [ petRespectRemaining.toString() ]) }
</ContextMenuListItemView> }
<ContextMenuListItemView onClick={ event => processAction('train') }>
{ LocalizeText('infostand.button.train') }
@ -189,9 +178,9 @@ export const AvatarInfoWidgetOwnPetView: FC<AvatarInfoWidgetOwnPetViewProps> = p
<ContextMenuListItemView onClick={ event => processAction('dismount') }>
{ LocalizeText('infostand.button.dismount') }
</ContextMenuListItemView>
{ (respectsLeft > 0) &&
{ (petRespectRemaining > 0) &&
<ContextMenuListItemView onClick={ event => processAction('respect') }>
{ LocalizeText('infostand.button.petrespect', [ 'count' ], [ respectsLeft.toString() ]) }
{ LocalizeText('infostand.button.petrespect', [ 'count' ], [ petRespectRemaining.toString() ]) }
</ContextMenuListItemView> }
</> }
{ (mode === MODE_MONSTER_PLANT) &&
@ -209,7 +198,7 @@ export const AvatarInfoWidgetOwnPetView: FC<AvatarInfoWidgetOwnPetViewProps> = p
</ContextMenuListItemView> }
{ !avatarInfo.dead && ((avatarInfo.energy / avatarInfo.maximumEnergy) < 0.98) &&
<ContextMenuListItemView onClick={ event => processAction('treat') }>
{ LocalizeText('infostand.button.treat') }
{ LocalizeText('infostand.button.pettreat') }
</ContextMenuListItemView> }
{ !avatarInfo.dead && (avatarInfo.level === avatarInfo.maximumLevel) && avatarInfo.breedable &&
<>

View File

@ -1,7 +1,7 @@
import { PetRespectComposer, PetType, RoomControllerLevel, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomUnitGiveHandItemPetComposer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useState } from 'react';
import { AvatarInfoPet, GetOwnRoomObject, GetSessionDataManager, LocalizeText, SendMessageComposer } from '../../../../../api';
import { useRoom } from '../../../../../hooks';
import { useRoom, useSessionInfo } from '../../../../../hooks';
import { ContextMenuHeaderView } from '../../context-menu/ContextMenuHeaderView';
import { ContextMenuListItemView } from '../../context-menu/ContextMenuListItemView';
import { ContextMenuView } from '../../context-menu/ContextMenuView';
@ -21,8 +21,8 @@ export const AvatarInfoWidgetPetView: FC<AvatarInfoWidgetPetViewProps> = props =
{
const { avatarInfo = null, onClose = null } = props;
const [ mode, setMode ] = useState(MODE_NORMAL);
const [ respectsLeft, setRespectsLeft ] = useState(0);
const { roomSession = null } = useRoom();
const { petRespectRemaining = 0, respectPet = null } = useSessionInfo();
const canPickUp = useMemo(() =>
{
@ -54,18 +54,9 @@ export const AvatarInfoWidgetPetView: FC<AvatarInfoWidgetPetViewProps> = props =
switch(name)
{
case 'respect':
let newRespectsLeft = 0;
respectPet(avatarInfo.id);
setRespectsLeft(prevValue =>
{
newRespectsLeft = (prevValue - 1);
return newRespectsLeft;
});
GetSessionDataManager().givePetRespect(avatarInfo.id);
if(newRespectsLeft > 0) hideMenu = false;
if((petRespectRemaining - 1) >= 1) hideMenu = false;
break;
case 'treat':
SendMessageComposer(new PetRespectComposer(avatarInfo.id));
@ -98,8 +89,6 @@ export const AvatarInfoWidgetPetView: FC<AvatarInfoWidgetPetViewProps> = props =
return MODE_NORMAL;
});
setRespectsLeft(avatarInfo.respectsPetLeft);
}, [ avatarInfo ]);
return (
@ -107,9 +96,9 @@ export const AvatarInfoWidgetPetView: FC<AvatarInfoWidgetPetViewProps> = props =
<ContextMenuHeaderView>
{ avatarInfo.name }
</ContextMenuHeaderView>
{ (mode === MODE_NORMAL) && (respectsLeft > 0) &&
{ (mode === MODE_NORMAL) && (petRespectRemaining > 0) &&
<ContextMenuListItemView onClick={ event => processAction('respect') }>
{ LocalizeText('infostand.button.petrespect', [ 'count' ], [ respectsLeft.toString() ]) }
{ LocalizeText('infostand.button.petrespect', [ 'count' ], [ petRespectRemaining.toString() ]) }
</ContextMenuListItemView> }
{ (mode === MODE_SADDLED_UP) &&
<>
@ -117,9 +106,9 @@ export const AvatarInfoWidgetPetView: FC<AvatarInfoWidgetPetViewProps> = props =
<ContextMenuListItemView onClick={ event => processAction('mount') }>
{ LocalizeText('infostand.button.mount') }
</ContextMenuListItemView> }
{ (respectsLeft > 0) &&
{ (petRespectRemaining > 0) &&
<ContextMenuListItemView onClick={ event => processAction('respect') }>
{ LocalizeText('infostand.button.petrespect', [ 'count' ], [ respectsLeft.toString() ]) }
{ LocalizeText('infostand.button.petrespect', [ 'count' ], [ petRespectRemaining.toString() ]) }
</ContextMenuListItemView> }
</> }
{ (mode === MODE_RIDING) &&
@ -127,14 +116,14 @@ export const AvatarInfoWidgetPetView: FC<AvatarInfoWidgetPetViewProps> = props =
<ContextMenuListItemView onClick={ event => processAction('dismount') }>
{ LocalizeText('infostand.button.dismount') }
</ContextMenuListItemView>
{ (respectsLeft > 0) &&
{ (petRespectRemaining > 0) &&
<ContextMenuListItemView onClick={ event => processAction('respect') }>
{ LocalizeText('infostand.button.petrespect', [ 'count' ], [ respectsLeft.toString() ]) }
{ LocalizeText('infostand.button.petrespect', [ 'count' ], [ petRespectRemaining.toString() ]) }
</ContextMenuListItemView> }
</> }
{ (mode === MODE_MONSTER_PLANT) && !avatarInfo.dead && ((avatarInfo.energy / avatarInfo.maximumEnergy) < 0.98) &&
<ContextMenuListItemView onClick={ event => processAction('treat') }>
{ LocalizeText('infostand.button.treat') }
{ LocalizeText('infostand.button.pettreat') }
</ContextMenuListItemView> }
{ canPickUp &&
<ContextMenuListItemView onClick={ event => processAction('pick_up') }>

View File

@ -89,8 +89,15 @@ export const ChatInputView: FC<{}> = props =>
if(text.length <= maxChatLength)
{
setChatValue('');
sendChat(text, chatType, recipientName, chatStyleId);
if(/%CC%/g.test(encodeURIComponent(text)))
{
setChatValue('');
}
else
{
setChatValue('');
sendChat(text, chatType, recipientName, chatStyleId);
}
}
setChatValue(append);
@ -141,7 +148,7 @@ export const ChatInputView: FC<{}> = props =>
}
return;
}
}, [ floodBlocked, inputRef, chatModeIdWhisper, anotherInputHasFocus, setInputFocus, checkSpecialKeywordForInput, sendChatValue ]);
useUiEvent<RoomWidgetUpdateChatInputContentEvent>(RoomWidgetUpdateChatInputContentEvent.CHAT_INPUT_CONTENT, event =>

View File

@ -1,14 +1,24 @@
import { FC } from 'react';
import { CreateLinkEvent, LocalizeText } from '../../../../api';
import { attemptItemPlacement, CreateLinkEvent, LocalizeText } from '../../../../api';
import { Button, Column, Flex, LayoutGiftTagView, LayoutImage, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useFurniturePresentWidget } from '../../../../hooks';
import { useFurniturePresentWidget, useInventoryFurni } from '../../../../hooks';
export const FurnitureGiftOpeningView: FC<{}> = props =>
{
const { objectId = -1, classId = -1, itemType = null, text = null, isOwnerOfFurniture = false, senderName = null, senderFigure = null, placedItemId = -1, placedItemType = null, placedInRoom = false, imageUrl = null, openPresent = null, onClose = null } = useFurniturePresentWidget();
const { groupItems = [] } = useInventoryFurni();
if(objectId === -1) return null;
const place = (itemId: number) =>
{
const groupItem = groupItems.find(group => (group.getItemById(itemId)?.id === itemId));
if(groupItem) attemptItemPlacement(groupItem);
onClose();
}
return (
<NitroCardView className="nitro-gift-opening" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText(senderName ? 'widget.furni.present.window.title_from' : 'widget.furni.present.window.title', [ 'name' ], [ senderName ]) } onCloseClick={ onClose } />
@ -45,7 +55,7 @@ export const FurnitureGiftOpeningView: FC<{}> = props =>
<Button fullWidth onClick={ null }>
{ LocalizeText('widget.furni.present.put_in_inventory') }
</Button> }
<Button fullWidth variant="success" onClick={ null }>
<Button fullWidth variant="success" onClick={ event => place(placedItemId) }>
{ LocalizeText(placedInRoom ? 'widget.furni.present.keep_in_room' : 'widget.furni.present.place_in_room') }
</Button>
</Flex>

View File

@ -1,5 +1,5 @@
import { FurnitureStackHeightComposer } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { FC, useEffect, useState } from 'react';
import ReactSlider from 'react-slider';
import { LocalizeText, SendMessageComposer } from '../../../../api';
import { Button, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
@ -8,6 +8,23 @@ import { useFurnitureStackHeightWidget } from '../../../../hooks';
export const FurnitureStackHeightView: FC<{}> = props =>
{
const { objectId = -1, height = 0, maxHeight = 40, onClose = null, updateHeight = null } = useFurnitureStackHeightWidget();
const [ tempHeight, setTempHeight ] = useState('');
const updateTempHeight = (value: string) =>
{
setTempHeight(value);
const newValue = parseFloat(value);
if(isNaN(newValue) || (newValue === height)) return;
updateHeight(newValue);
}
useEffect(() =>
{
setTempHeight(height.toString());
}, [ height ]);
if(objectId === -1) return null;
@ -25,7 +42,7 @@ export const FurnitureStackHeightView: FC<{}> = props =>
value={ height }
onChange={ event => updateHeight(event) }
renderThumb={ (props, state) => <div { ...props }>{ state.valueNow }</div> } />
<input className="show-number-arrows" type="number" min={ 0 } max={ maxHeight } value={ height } onChange={ event => updateHeight(parseFloat(event.target.value)) } />
<input className="show-number-arrows" style={ { width: 50 } } type="number" min={ 0 } max={ maxHeight } value={ tempHeight } onChange={ event => updateTempHeight(event.target.value) } />
</Flex>
<Column gap={ 1 }>
<Button onClick={ event => SendMessageComposer(new FurnitureStackHeightComposer(objectId, -100)) }>

View File

@ -5,6 +5,8 @@ import { useFurnitureStickieWidget } from '../../../../hooks';
const STICKIE_COLORS = [ '9CCEFF','FF9CFF', '9CFF9C','FFFF33' ];
const STICKIE_COLOR_NAMES = [ 'blue', 'pink', 'green', 'yellow' ];
const STICKIE_TYPES = [ 'post_it','post_it_shakesp', 'post_it_dreams','post_it_xmas', 'post_it_vd', 'post_it_juninas' ];
const STICKIE_TYPE_NAMES = [ 'post_it', 'shakesp', 'dreams', 'christmas', 'heart', 'juninas' ];
const getStickieColorName = (color: string) =>
{
@ -15,30 +17,42 @@ const getStickieColorName = (color: string) =>
return STICKIE_COLOR_NAMES[index];
}
const getStickieTypeName = (type: string) =>
{
let index = STICKIE_TYPES.indexOf(type);
if(index === -1) index = 0;
return STICKIE_TYPE_NAMES[index];
}
export const FurnitureStickieView: FC<{}> = props =>
{
const { objectId = -1, color = '0', text = '', canModify = false, updateColor = null, updateText = null, trash = null, onClose = null } = useFurnitureStickieWidget();
const { objectId = -1, color = '0', text = '', type = '', canModify = false, updateColor = null, updateText = null, trash = null, onClose = null } = useFurnitureStickieWidget();
const [ isEditing, setIsEditing ] = useState(false);
useEffect(() =>
{
setIsEditing(false);
}, [ objectId, color, text ]);
}, [ objectId, color, text, type ]);
if(objectId === -1) return null;
return (
<DraggableWindow handleSelector=".drag-handler" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<div className={ 'nitro-stickie nitro-stickie-image stickie-' + getStickieColorName(color) }>
<div className={ 'nitro-stickie nitro-stickie-image stickie-' + (type == 'post_it' ? getStickieColorName(color) : getStickieTypeName(type)) }>
<div className="d-flex align-items-center stickie-header drag-handler">
<div className="d-flex align-items-center flex-grow-1 h-100">
{ canModify &&
{ canModify &&
<>
<div className="nitro-stickie-image stickie-trash header-trash" onClick={ trash }></div>
{ STICKIE_COLORS.map(color =>
{
return <div key={ color } className="stickie-color ms-1" onClick={ event => updateColor(color) } style={ { backgroundColor: ColorUtils.makeColorHex(color) } } />
}) }
{ type == 'post_it' &&
<>
{ STICKIE_COLORS.map(color =>
{
return <div key={ color } className="stickie-color ms-1" onClick={ event => updateColor(color) } style={ { backgroundColor: ColorUtils.makeColorHex(color) } } />
}) }
</> }
</> }
</div>
<div className="d-flex align-items-center nitro-stickie-image stickie-close header-close" onClick={ onClose }></div>

View File

@ -161,6 +161,26 @@
background-position: -2px -184px;
}
&.stickie-christmas {
background-image: url("../../../../assets/images/room-widgets/stickie-widget/stickie-christmas.png");
}
&.stickie-shakesp {
background-image: url("../../../../assets/images/room-widgets/stickie-widget/stickie-shakesp.png");
}
&.stickie-dreams {
background-image: url("../../../../assets/images/room-widgets/stickie-widget/stickie-dreams.png");
}
&.stickie-heart {
background-image: url("../../../../assets/images/room-widgets/stickie-widget/stickie-heart.png");
}
&.stickie-juninas {
background-image: url("../../../../assets/images/room-widgets/stickie-widget/stickie-juninas.png");
}
&.stickie-close {
width: 10px;
height: 10px;
@ -202,7 +222,7 @@
font-weight: bold;
font-size: 16px;
text-shadow: 0px 1px white;
&.engraving-lock-3 {
background-position: 0px -210px;
color: #614110;
@ -262,7 +282,7 @@
.youtube-video-container {
//min-height: 366px;
.empty-video {
background-color: black;
color: white;

View File

@ -19,7 +19,7 @@ export const WiredBaseView: FC<PropsWithChildren<WiredBaseViewProps>> = props =>
const [ wiredName, setWiredName ] = useState<string>(null);
const [ wiredDescription, setWiredDescription ] = useState<string>(null);
const [ needsSave, setNeedsSave ] = useState<boolean>(false);
const { trigger = null, setTrigger = null, setIntParams = null, setStringParam = null, setFurniIds = null, saveWired = null } = useWired();
const { trigger = null, setTrigger = null, setIntParams = null, setStringParam = null, setFurniIds = null, setAllowsFurni = null, saveWired = null } = useWired();
const onClose = () => setTrigger(null);
@ -83,6 +83,11 @@ export const WiredBaseView: FC<PropsWithChildren<WiredBaseViewProps>> = props =>
}
}, [ trigger, hasSpecialInput, requiresFurni, setIntParams, setStringParam, setFurniIds ]);
useEffect(() =>
{
setAllowsFurni(requiresFurni);
}, [ requiresFurni, setAllowsFurni ]);
return (
<NitroCardView uniqueKey="nitro-wired" className="nitro-wired" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('wiredfurni.title') } onCloseClick={ onClose } />

View File

@ -1,5 +1,5 @@
import { FC, useEffect, useState } from 'react';
import { LocalizeText, WiredFurniType, WIRED_STRING_DELIMETER } from '../../../../api';
import { GetConfiguration, LocalizeText, WiredFurniType, WIRED_STRING_DELIMETER } from '../../../../api';
import { Column, Flex, Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredActionBaseView } from './WiredActionBaseView';
@ -35,7 +35,7 @@ export const WiredActionBotTalkToAvatarView: FC<{}> = props =>
</Column>
<Column gap={ 1 }>
<Text bold>{ LocalizeText('wiredfurni.params.message') }</Text>
<input type="text" className="form-control form-control-sm" maxLength={ 64 } value={ message } onChange={ event => setMessage(event.target.value) } />
<input type="text" className="form-control form-control-sm" maxLength={ GetConfiguration<number>('wired.action.bot.talk.to.avatar.max.length', 64) } value={ message } onChange={ event => setMessage(event.target.value) } />
</Column>
<Column gap={ 1 }>
<Flex alignItems="center" gap={ 1 }>

View File

@ -1,5 +1,5 @@
import { FC, useEffect, useState } from 'react';
import { LocalizeText, WiredFurniType, WIRED_STRING_DELIMETER } from '../../../../api';
import { GetConfiguration, LocalizeText, WiredFurniType, WIRED_STRING_DELIMETER } from '../../../../api';
import { Column, Flex, Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredActionBaseView } from './WiredActionBaseView';
@ -35,7 +35,7 @@ export const WiredActionBotTalkView: FC<{}> = props =>
</Column>
<Column gap={ 1 }>
<Text bold>{ LocalizeText('wiredfurni.params.message') }</Text>
<input type="text" className="form-control form-control-sm" maxLength={ 64 } value={ message } onChange={ event => setMessage(event.target.value) } />
<input type="text" className="form-control form-control-sm" maxLength={ GetConfiguration<number>('wired.action.bot.talk.max.length', 64) } value={ message } onChange={ event => setMessage(event.target.value) } />
</Column>
<Column gap={ 1 }>
<Flex alignItems="center" gap={ 1 }>

View File

@ -1,5 +1,5 @@
import { FC, useEffect, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { GetConfiguration, LocalizeText, WiredFurniType } from '../../../../api';
import { Column, Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredActionBaseView } from './WiredActionBaseView';
@ -20,7 +20,7 @@ export const WiredActionChatView: FC<{}> = props =>
<WiredActionBaseView requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } hasSpecialInput={ true } save={ save }>
<Column gap={ 1 }>
<Text bold>{ LocalizeText('wiredfurni.params.message') }</Text>
<input type="text" className="form-control form-control-sm" value={ message } onChange={ event => setMessage(event.target.value) } maxLength={ 100 } />
<input type="text" className="form-control form-control-sm" value={ message } onChange={ event => setMessage(event.target.value) } maxLength={ GetConfiguration<number>('wired.action.chat.max.length', 100) } />
</Column>
</WiredActionBaseView>
);

View File

@ -1,5 +1,5 @@
import { FC, useEffect, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { GetConfiguration, LocalizeText, WiredFurniType } from '../../../../api';
import { Column, Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredActionBaseView } from './WiredActionBaseView';
@ -20,7 +20,7 @@ export const WiredActionKickFromRoomView: FC<{}> = props =>
<WiredActionBaseView requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } hasSpecialInput={ true } save={ save }>
<Column gap={ 1 }>
<Text bold>{ LocalizeText('wiredfurni.params.message') }</Text>
<input type="text" className="form-control form-control-sm" value={ message } onChange={ event => setMessage(event.target.value) } maxLength={ 100 } />
<input type="text" className="form-control form-control-sm" value={ message } onChange={ event => setMessage(event.target.value) } maxLength={ GetConfiguration<number>('wired.action.kick.from.room.max.length', 100) } />
</Column>
</WiredActionBaseView>
);

View File

@ -1,6 +1,6 @@
import { FC, useEffect, useState } from 'react';
import ReactSlider from 'react-slider';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { GetConfiguration, LocalizeText, WiredFurniType } from '../../../../api';
import { Column, Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredActionBaseView } from './WiredActionBaseView';
@ -36,7 +36,7 @@ export const WiredActionMuteUserView: FC<{}> = props =>
</Column>
<Column gap={ 1 }>
<Text bold>{ LocalizeText('wiredfurni.params.message') }</Text>
<input type="text" className="form-control form-control-sm" value={ message } onChange={ event => setMessage(event.target.value) } maxLength={ 100 } />
<input type="text" className="form-control form-control-sm" value={ message } onChange={ event => setMessage(event.target.value) } maxLength={ GetConfiguration<number>('wired.action.mute.user.max.length', 100) } />
</Column>
</WiredActionBaseView>
);

View File

@ -835,7 +835,7 @@ const useCatalogState = () =>
useEffect(() =>
{
if(!isVisible || !rootNode || !requestedPage.current) return;
if(!isVisible || !rootNode || !offersToNodes || !requestedPage.current) return;
switch(requestedPage.current.requestType)
{
@ -868,12 +868,19 @@ const useCatalogState = () =>
requestedPage.current.resetRequest();
return;
}
}, [ isVisible, rootNode, currentPage, activateNode, openPageById, openPageByOfferId, openPageByName ]);
}, [ isVisible, rootNode, offersToNodes, currentPage, activateNode, openPageById, openPageByOfferId, openPageByName ]);
useEffect(() =>
{
if(!searchResult && currentPage && (currentPage.pageId === -1)) openPageById(previousPageId);
}, [ searchResult, currentPage, previousPageId, openPageById ]);
useEffect(() =>
{
if(!currentOffer) return;
setPurchaseOptions({ quantity: 1, extraData: null, extraParamRequired: false, previewStuffData: null });
}, [ currentOffer ]);
useEffect(() =>
{

View File

@ -24,7 +24,7 @@ const useFurnitureStackHeightWidgetState = () =>
const updateHeight = (height: number, server: boolean = false) =>
{
if(!height) height = 0;
height = Math.abs(height);
if(!server) ((height > MAX_HEIGHT) && (height = MAX_HEIGHT));

View File

@ -10,6 +10,7 @@ const useFurnitureStickieWidgetState = () =>
const [ category, setCategory ] = useState(-1);
const [ color, setColor ] = useState('0');
const [ text, setText ] = useState('');
const [ type, setType ] = useState('');
const [ canModify, setCanModify ] = useState(false);
const onClose = () =>
@ -18,6 +19,7 @@ const useFurnitureStickieWidgetState = () =>
setCategory(-1);
setColor('0');
setText('');
setType('');
setCanModify(false);
}
@ -44,7 +46,7 @@ const useFurnitureStickieWidgetState = () =>
const roomObject = GetRoomEngine().getRoomObject(event.roomId, event.objectId, event.category);
if(!roomObject) return;
const data = roomObject.model.getValue<string>(RoomObjectVariable.FURNITURE_ITEMDATA);
if(data.length < 6) return;
@ -66,6 +68,7 @@ const useFurnitureStickieWidgetState = () =>
setCategory(event.category);
setColor(color || '0');
setText(text || '');
setType(roomObject.type || 'post_it');
setCanModify(GetRoomSession().isRoomOwner || GetSessionDataManager().isModerator || IsOwnerOfFurniture(roomObject));
});
@ -76,7 +79,7 @@ const useFurnitureStickieWidgetState = () =>
onClose();
});
return { objectId, color, text, canModify, updateColor, updateText, trash, onClose };
return { objectId, color, text, type, canModify, updateColor, updateText, trash, onClose };
}
export const useFurnitureStickieWidget = useFurnitureStickieWidgetState;

View File

@ -1,14 +1,18 @@
import { FigureUpdateEvent, RoomUnitChatStyleComposer, UserInfoDataParser, UserInfoEvent, UserSettingsEvent } from '@nitrots/nitro-renderer';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useBetween } from 'use-between';
import { SendMessageComposer } from '../../api';
import { GetLocalStorage, GetSessionDataManager, SendMessageComposer } from '../../api';
import { useMessageEvent } from '../events';
import { useLocalStorage } from '../useLocalStorage';
const useSessionInfoState = () =>
{
const [ userInfo, setUserInfo ] = useState<UserInfoDataParser>(null);
const [ userFigure, setUserFigure ] = useState<string>(null);
const [ chatStyleId, setChatStyleId ] = useState<number>(0);
const [ userRespectRemaining, setUserRespectRemaining ] = useState<number>(0);
const [ petRespectRemaining, setPetRespectRemaining ] = useState<number>(0);
const [ screenSize, setScreenSize ] = useLocalStorage('nitro.screensize', { width: window.innerWidth, height: window.innerHeight });
const updateChatStyleId = (styleId: number) =>
{
@ -17,12 +21,28 @@ const useSessionInfoState = () =>
SendMessageComposer(new RoomUnitChatStyleComposer(styleId));
}
const respectUser = (userId: number) =>
{
GetSessionDataManager().giveRespect(userId);
setUserRespectRemaining(GetSessionDataManager().respectsLeft);
}
const respectPet = (petId: number) =>
{
GetSessionDataManager().givePetRespect(petId);
setPetRespectRemaining(GetSessionDataManager().respectsPetLeft);
}
useMessageEvent<UserInfoEvent>(UserInfoEvent, event =>
{
const parser = event.getParser();
setUserInfo(parser.userInfo);
setUserFigure(parser.userInfo.figure);
setUserRespectRemaining(parser.userInfo.respectsRemaining);
setPetRespectRemaining(parser.userInfo.respectsPetRemaining);
});
useMessageEvent<FigureUpdateEvent>(FigureUpdateEvent, event =>
@ -39,7 +59,35 @@ const useSessionInfoState = () =>
setChatStyleId(parser.chatType);
});
return { userInfo, userFigure, chatStyleId, updateChatStyleId };
useEffect(() =>
{
const currentScreenSize = <{ width: number, height: number }>GetLocalStorage('nitro.screensize');
if(currentScreenSize && ((currentScreenSize.width !== window.innerWidth) || (currentScreenSize.height !== window.innerHeight)))
{
let i = window.localStorage.length;
while(i > 0)
{
const key = window.localStorage.key(i);
if(key && key.startsWith('nitro.window')) window.localStorage.removeItem(key);
i--;
}
}
const onResize = (event: UIEvent) => setScreenSize({ width: window.innerWidth, height: window.innerHeight });
window.addEventListener('resize', onResize);
return () =>
{
window.removeEventListener('resize', onResize);
}
}, [ setScreenSize ]);
return { userInfo, userFigure, chatStyleId, userRespectRemaining, petRespectRemaining, respectUser, respectPet, updateChatStyleId };
}
export const useSessionInfo = () => useBetween(useSessionInfoState);

View File

@ -1,5 +1,6 @@
import { NitroLogger } from '@nitrots/nitro-renderer';
import { Dispatch, SetStateAction, useState } from 'react';
import { GetLocalStorage, SetLocalStorage } from '../api';
const useLocalStorageState = <T>(key: string, initialValue: T): [ T, Dispatch<SetStateAction<T>>] =>
{
@ -9,9 +10,9 @@ const useLocalStorageState = <T>(key: string, initialValue: T): [ T, Dispatch<Se
try
{
const item = window.localStorage.getItem(key);
const item = GetLocalStorage<T>(key);
return item ? JSON.parse(item) : initialValue;
return item ?? initialValue;
}
catch(error)
@ -28,7 +29,7 @@ const useLocalStorageState = <T>(key: string, initialValue: T): [ T, Dispatch<Se
setStoredValue(valueToStore);
if(typeof window !== 'undefined') window.localStorage.setItem(key, JSON.stringify(valueToStore));
if(typeof window !== 'undefined') SetLocalStorage(key, valueToStore);
}
catch(error)

View File

@ -1,7 +1,7 @@
import { ConditionDefinition, Triggerable, TriggerDefinition, UpdateActionMessageComposer, UpdateConditionMessageComposer, UpdateTriggerMessageComposer, WiredActionDefinition, WiredFurniActionEvent, WiredFurniConditionEvent, WiredFurniTriggerEvent, WiredSaveSuccessEvent } from '@nitrots/nitro-renderer';
import { useEffect, useState } from 'react';
import { useBetween } from 'use-between';
import { IsOwnerOfFloorFurniture, LocalizeText, SendMessageComposer, WiredSelectionVisualizer } from '../../api';
import { IsOwnerOfFloorFurniture, LocalizeText, SendMessageComposer, WiredFurniType, WiredSelectionVisualizer } from '../../api';
import { useMessageEvent } from '../events';
import { useNotification } from '../notification';
@ -12,6 +12,7 @@ const useWiredState = () =>
const [ stringParam, setStringParam ] = useState<string>('');
const [ furniIds, setFurniIds ] = useState<number[]>([]);
const [ actionDelay, setActionDelay ] = useState<number>(0);
const [ allowsFurni, setAllowsFurni ] = useState<number>(WiredFurniType.STUFF_SELECTION_OPTION_NONE);
const { showConfirm = null } = useNotification();
const saveWired = () =>
@ -51,7 +52,7 @@ const useWiredState = () =>
const selectObjectForWired = (objectId: number, category: number) =>
{
if(!trigger) return;
if(!trigger || !allowsFurni) return;
if(objectId <= 0) return;
@ -122,10 +123,11 @@ const useWiredState = () =>
return [];
});
setAllowsFurni(WiredFurniType.STUFF_SELECTION_OPTION_NONE);
}
}, [ trigger ]);
return { trigger, setTrigger, intParams, setIntParams, stringParam, setStringParam, furniIds, setFurniIds, actionDelay, setActionDelay, saveWired, selectObjectForWired };
return { trigger, setTrigger, intParams, setIntParams, stringParam, setStringParam, furniIds, setFurniIds, actionDelay, setActionDelay, setAllowsFurni, saveWired, selectObjectForWired };
}
export const useWired = () => useBetween(useWiredState);