Merge branch 'dev' into @update/groups

This commit is contained in:
Bill 2022-02-22 11:09:07 -05:00
commit 54312a412e
165 changed files with 2123 additions and 2344 deletions

View File

@ -8,6 +8,7 @@
"habbopages.url": "https://website.com/habbopages/", "habbopages.url": "https://website.com/habbopages/",
"chat.viewer.height.percentage": 0.40, "chat.viewer.height.percentage": 0.40,
"widget.dimmer.colorwheel": false, "widget.dimmer.colorwheel": false,
"avatar.wardrobe.max.slots": 10,
"hotelview": { "hotelview": {
"widgets": { "widgets": {
"slot.1.widget": "promoarticle", "slot.1.widget": "promoarticle",

View File

@ -70,6 +70,7 @@ $camera-checkout-width: 350px;
$room-info-width: 325px; $room-info-width: 325px;
$nitro-group-creator-width: 383px; $nitro-group-creator-width: 383px;
$nitro-mod-tools-width: 175px;
.nitro-app { .nitro-app {
width: 100%; width: 100%;

View File

@ -1,13 +1,14 @@
import { ConfigurationEvent, HabboWebTools, LegacyExternalInterface, Nitro, NitroCommunicationDemoEvent, NitroEvent, NitroLocalizationEvent, NitroVersion, RoomEngineEvent, WebGL } from '@nitrots/nitro-renderer'; import { ConfigurationEvent, HabboWebTools, LegacyExternalInterface, Nitro, NitroCommunicationDemoEvent, NitroEvent, NitroLocalizationEvent, NitroVersion, RoomEngineEvent, WebGL } from '@nitrots/nitro-renderer';
import { FC, useCallback, useState } from 'react'; import { FC, useCallback, useState } from 'react';
import { GetCommunication, GetConfiguration, GetNitroInstance } from './api'; import { GetCommunication, GetConfiguration, GetNitroInstance } from './api';
import { Base } from './common';
import { LoadingView } from './components/loading/LoadingView';
import { MainView } from './components/main/MainView';
import { useConfigurationEvent } from './hooks/events/core/configuration/configuration-event'; import { useConfigurationEvent } from './hooks/events/core/configuration/configuration-event';
import { useLocalizationEvent } from './hooks/events/nitro/localization/localization-event'; import { useLocalizationEvent } from './hooks/events/nitro/localization/localization-event';
import { dispatchMainEvent, useMainEvent } from './hooks/events/nitro/main-event'; import { dispatchMainEvent, useMainEvent } from './hooks/events/nitro/main-event';
import { useRoomEngineEvent } from './hooks/events/nitro/room/room-engine-event'; import { useRoomEngineEvent } from './hooks/events/nitro/room/room-engine-event';
import { TransitionAnimation, TransitionAnimationTypes } from './layout'; import { TransitionAnimation, TransitionAnimationTypes } from './layout';
import { LoadingView } from './views/loading/LoadingView';
import { MainView } from './views/main/MainView';
export const App: FC<{}> = props => export const App: FC<{}> = props =>
{ {
@ -127,12 +128,13 @@ export const App: FC<{}> = props =>
} }
return ( return (
<div className="nitro-app overflow-hidden"> <Base fit overflow="hidden">
{ (!isReady || isError) && <LoadingView isError={ isError } message={ message } /> } { (!isReady || isError) &&
<LoadingView isError={ isError } message={ message } /> }
<TransitionAnimation type={ TransitionAnimationTypes.FADE_IN } inProp={ (isReady && !isError) }> <TransitionAnimation type={ TransitionAnimationTypes.FADE_IN } inProp={ (isReady && !isError) }>
<MainView /> <MainView />
</TransitionAnimation> </TransitionAnimation>
<div id="draggable-windows-container" /> <Base id="draggable-windows-container" />
</div> </Base>
); );
} }

View File

@ -79,6 +79,11 @@
font-weight: $font-weight-normal; font-weight: $font-weight-normal;
color: $btn-link-color; color: $btn-link-color;
text-decoration: $link-decoration; text-decoration: $link-decoration;
box-shadow: none !important;
&:active {
color: $btn-link-color !important;
}
&:hover { &:hover {
color: $btn-link-hover-color; color: $btn-link-hover-color;

View File

@ -732,7 +732,7 @@ $table-cell-padding-x-sm: .25rem !default;
$table-cell-vertical-align: top !default; $table-cell-vertical-align: top !default;
$table-color: $body-color !default; $table-color: $black !default;
$table-bg: transparent !default; $table-bg: transparent !default;
$table-accent-bg: transparent !default; $table-accent-bg: transparent !default;

View File

@ -90,3 +90,10 @@ ul {
.flex-basis-max-content { .flex-basis-max-content {
flex-basis: max-content; flex-basis: max-content;
} }
.striped-children {
> :nth-child(1) {
background-color: $table-striped-bg;
}
}

View File

@ -1,5 +1,5 @@
import { CSSProperties, DetailedHTMLProps, FC, HTMLAttributes, LegacyRef, useMemo } from 'react'; import { CSSProperties, DetailedHTMLProps, FC, HTMLAttributes, LegacyRef, useMemo } from 'react';
import { ColorVariantType, OverflowType, PositionType } from './types'; import { ColorVariantType, FloatType, OverflowType, PositionType } from './types';
export interface BaseProps<T = HTMLElement> extends DetailedHTMLProps<HTMLAttributes<T>, T> export interface BaseProps<T = HTMLElement> extends DetailedHTMLProps<HTMLAttributes<T>, T>
{ {
@ -11,6 +11,7 @@ export interface BaseProps<T = HTMLElement> extends DetailedHTMLProps<HTMLAttrib
fullHeight?: boolean; fullHeight?: boolean;
overflow?: OverflowType; overflow?: OverflowType;
position?: PositionType; position?: PositionType;
float?: FloatType;
pointer?: boolean; pointer?: boolean;
textColor?: ColorVariantType; textColor?: ColorVariantType;
classNames?: string[]; classNames?: string[];
@ -18,7 +19,7 @@ export interface BaseProps<T = HTMLElement> extends DetailedHTMLProps<HTMLAttrib
export const Base: FC<BaseProps<HTMLDivElement>> = props => export const Base: FC<BaseProps<HTMLDivElement>> = props =>
{ {
const { ref = null, innerRef = null, fit = false, grow = false, shrink = false, fullWidth = false, fullHeight = false, overflow = null, position = null, pointer = false, textColor = null, classNames = [], className = '', style = {}, ...rest } = props; const { ref = null, innerRef = null, fit = false, grow = false, shrink = false, fullWidth = false, fullHeight = false, overflow = null, position = null, float = null, pointer = false, textColor = null, classNames = [], className = '', style = {}, ...rest } = props;
const getClassNames = useMemo(() => const getClassNames = useMemo(() =>
{ {
@ -36,6 +37,8 @@ export const Base: FC<BaseProps<HTMLDivElement>> = props =>
if(position) newClassNames.push('position-' + position); if(position) newClassNames.push('position-' + position);
if(float) newClassNames.push('float-' + float);
if(pointer) newClassNames.push('cursor-pointer'); if(pointer) newClassNames.push('cursor-pointer');
if(textColor) newClassNames.push('text-' + textColor); if(textColor) newClassNames.push('text-' + textColor);
@ -43,7 +46,7 @@ export const Base: FC<BaseProps<HTMLDivElement>> = props =>
if(classNames.length) newClassNames.push(...classNames); if(classNames.length) newClassNames.push(...classNames);
return newClassNames; return newClassNames;
}, [ fit, grow, shrink, fullWidth, fullHeight, overflow, position, pointer, textColor, classNames ]); }, [ fit, grow, shrink, fullWidth, fullHeight, overflow, position, float, pointer, textColor, classNames ]);
const getClassName = useMemo(() => const getClassName = useMemo(() =>
{ {
@ -51,7 +54,7 @@ export const Base: FC<BaseProps<HTMLDivElement>> = props =>
if(className.length) newClassName += (' ' + className); if(className.length) newClassName += (' ' + className);
return newClassName; return newClassName.trim();
}, [ getClassNames, className ]); }, [ getClassNames, className ]);
const getStyle = useMemo(() => const getStyle = useMemo(() =>

View File

@ -2,7 +2,7 @@ import { FC, useMemo } from 'react';
import { CSSProperties } from 'styled-components'; import { CSSProperties } from 'styled-components';
import { Base, BaseProps } from './Base'; import { Base, BaseProps } from './Base';
import { GridContextProvider } from './GridContext'; import { GridContextProvider } from './GridContext';
import { SpacingType } from './types'; import { AlignItemType, AlignSelfType, JustifyContentType, SpacingType } from './types';
export interface GridProps extends BaseProps<HTMLDivElement> export interface GridProps extends BaseProps<HTMLDivElement>
{ {
@ -10,11 +10,15 @@ export interface GridProps extends BaseProps<HTMLDivElement>
gap?: SpacingType; gap?: SpacingType;
maxContent?: boolean; maxContent?: boolean;
columnCount?: number; columnCount?: number;
center?: boolean;
alignSelf?: AlignSelfType;
alignItems?: AlignItemType;
justifyContent?: JustifyContentType;
} }
export const Grid: FC<GridProps> = props => export const Grid: FC<GridProps> = props =>
{ {
const { inline = false, gap = 2, maxContent = false, columnCount = 0, fullHeight = true, classNames = [], style = {}, ...rest } = props; const { inline = false, gap = 2, maxContent = false, columnCount = 0, center = false, alignSelf = null, alignItems = null, justifyContent = null, fullHeight = true, classNames = [], style = {}, ...rest } = props;
const getClassNames = useMemo(() => const getClassNames = useMemo(() =>
{ {
@ -28,10 +32,18 @@ export const Grid: FC<GridProps> = props =>
if(maxContent) newClassNames.push('flex-basis-max-content'); if(maxContent) newClassNames.push('flex-basis-max-content');
if(alignSelf) newClassNames.push('align-self-' + alignSelf);
if(alignItems) newClassNames.push('align-items-' + alignItems);
if(justifyContent) newClassNames.push('justify-content-' + justifyContent);
if(!alignItems && !justifyContent && center) newClassNames.push('align-items-center', 'justify-content-center');
if(classNames.length) newClassNames.push(...classNames); if(classNames.length) newClassNames.push(...classNames);
return newClassNames; return newClassNames;
}, [ inline, gap, maxContent, classNames ]); }, [ inline, gap, maxContent, alignSelf, alignItems, justifyContent, center, classNames ]);
const getStyle = useMemo(() => const getStyle = useMemo(() =>
{ {

View File

@ -1,12 +1,13 @@
import { FC, useMemo } from 'react'; import { FC, useMemo } from 'react';
import { Base, BaseProps } from './Base'; import { Base, BaseProps } from './Base';
import { ColorVariantType, FontSizeType, FontWeightType } from './types'; import { ColorVariantType, FontSizeType, FontWeightType, TextAlignType } from './types';
export interface TextProps extends BaseProps<HTMLDivElement> export interface TextProps extends BaseProps<HTMLDivElement>
{ {
variant?: ColorVariantType; variant?: ColorVariantType;
fontWeight?: FontWeightType; fontWeight?: FontWeightType;
fontSize?: FontSizeType; fontSize?: FontSizeType;
align?: TextAlignType;
bold?: boolean; bold?: boolean;
underline?: boolean; underline?: boolean;
italics?: boolean; italics?: boolean;
@ -14,11 +15,14 @@ export interface TextProps extends BaseProps<HTMLDivElement>
center?: boolean; center?: boolean;
textEnd?: boolean; textEnd?: boolean;
small?: boolean; small?: boolean;
wrap?: boolean;
noWrap?: boolean;
textBreak?: boolean;
} }
export const Text: FC<TextProps> = props => export const Text: FC<TextProps> = props =>
{ {
const { variant = 'black', fontWeight = null, fontSize = 0, bold = false, underline = false, italics = false, truncate = false, center = false, textEnd = false, small = false, ...rest } = props; const { variant = 'black', fontWeight = null, fontSize = 0, align = null, bold = false, underline = false, italics = false, truncate = false, center = false, textEnd = false, small = false, wrap = false, noWrap = false, textBreak = false, ...rest } = props;
const getClassNames = useMemo(() => const getClassNames = useMemo(() =>
{ {
@ -32,6 +36,8 @@ export const Text: FC<TextProps> = props =>
if(fontSize) newClassNames.push('fs-' + fontSize); if(fontSize) newClassNames.push('fs-' + fontSize);
if(align) newClassNames.push('text-' + align);
if(underline) newClassNames.push('text-decoration-underline'); if(underline) newClassNames.push('text-decoration-underline');
if(italics) newClassNames.push('fst-italic'); if(italics) newClassNames.push('fst-italic');
@ -44,8 +50,14 @@ export const Text: FC<TextProps> = props =>
if(small) newClassNames.push('small'); if(small) newClassNames.push('small');
if(wrap) newClassNames.push('text-wrap');
if(noWrap) newClassNames.push('text-nowrap');
if(textBreak) newClassNames.push('text-break');
return newClassNames; return newClassNames;
}, [ variant, fontWeight, fontSize, bold, underline, italics, truncate, center, textEnd, small ]); }, [ variant, fontWeight, fontSize, align, bold, underline, italics, truncate, center, textEnd, small, wrap, noWrap, textBreak ]);
return <Base classNames={ getClassNames } { ...rest } />; return <Base classNames={ getClassNames } { ...rest } />;
} }

View File

@ -6,7 +6,7 @@
&.active { &.active {
border-color: $grid-active-border-color !important; border-color: $grid-active-border-color !important;
background-color: $grid-active-bg-color !important; background-color: $grid-active-bg-color;
} }
&.disabled { &.disabled {
@ -25,4 +25,19 @@
.avatar-image { .avatar-image {
background-position-y: -35px; background-position-y: -35px;
} }
&.has-highlight {
&:after {
content: "";
z-index: 2;
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50%;
background-color: $white;
opacity: 0.1;
}
}
} }

View File

@ -14,12 +14,13 @@ export interface LayoutGridItemProps extends ColumnProps
itemUniqueSoldout?: boolean; itemUniqueSoldout?: boolean;
itemUniqueNumber?: number; itemUniqueNumber?: number;
itemUnseen?: boolean; itemUnseen?: boolean;
itemHighlight?: boolean;
disabled?: boolean; disabled?: boolean;
} }
export const LayoutGridItem: FC<LayoutGridItemProps> = props => export const LayoutGridItem: FC<LayoutGridItemProps> = props =>
{ {
const { itemImage = undefined, itemColor = undefined, itemActive = false, itemCount = 1, itemCountMinimum = 1, itemUniqueSoldout = false, itemUniqueNumber = -2, itemUnseen = false, disabled = false, center = true, column = true, style = {}, classNames = [], position = 'relative', overflow = 'hidden', children = null, ...rest } = props; const { itemImage = undefined, itemColor = undefined, itemActive = false, itemCount = 1, itemCountMinimum = 1, itemUniqueSoldout = false, itemUniqueNumber = -2, itemUnseen = false, itemHighlight = false, disabled = false, center = true, column = true, style = {}, classNames = [], position = 'relative', overflow = 'hidden', children = null, ...rest } = props;
const getClassNames = useMemo(() => const getClassNames = useMemo(() =>
{ {
@ -33,6 +34,8 @@ export const LayoutGridItem: FC<LayoutGridItemProps> = props =>
if(itemUnseen) newClassNames.push('unseen'); if(itemUnseen) newClassNames.push('unseen');
if(itemHighlight) newClassNames.push('has-highlight');
if(disabled) newClassNames.push('disabled') if(disabled) newClassNames.push('disabled')
if(itemImage === null) newClassNames.push('icon', 'loading-icon'); if(itemImage === null) newClassNames.push('icon', 'loading-icon');
@ -40,7 +43,7 @@ export const LayoutGridItem: FC<LayoutGridItemProps> = props =>
if(classNames.length) newClassNames.push(...classNames); if(classNames.length) newClassNames.push(...classNames);
return newClassNames; return newClassNames;
}, [ itemActive, itemUniqueSoldout, itemUniqueNumber, itemUnseen, disabled, itemImage, classNames ]); }, [ itemActive, itemUniqueSoldout, itemUniqueNumber, itemUnseen, itemHighlight, disabled, itemImage, classNames ]);
const getStyle = useMemo(() => const getStyle = useMemo(() =>
{ {

View File

@ -0,0 +1 @@
export type FloatType = 'start' | 'end' | 'none';

View File

@ -1 +1 @@
export type FontSizeType = 1 | 2 | 3 | 4 | 5; export type FontSizeType = 1 | 2 | 3 | 4 | 5 | 6;

View File

@ -0,0 +1 @@
export type TextAlignType = 'start' | 'center' | 'end';

View File

@ -3,9 +3,11 @@ export * from './AlignSelfType';
export * from './ButtonSizeType'; export * from './ButtonSizeType';
export * from './ColorVariantType'; export * from './ColorVariantType';
export * from './ColumnSizesType'; export * from './ColumnSizesType';
export * from './FloatType';
export * from './FontSizeType'; export * from './FontSizeType';
export * from './FontWeightType'; export * from './FontWeightType';
export * from './JustifyContentType'; export * from './JustifyContentType';
export * from './OverflowType'; export * from './OverflowType';
export * from './PositionType'; export * from './PositionType';
export * from './SpacingType'; export * from './SpacingType';
export * from './TextAlignType';

View File

@ -1,7 +1,7 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { AvatarEditorFigureCategory, FigureSetIdsMessageEvent, GetWardrobeMessageComposer, IAvatarFigureContainer, UserFigureComposer, UserWardrobePageEvent } from '@nitrots/nitro-renderer'; import { AvatarEditorFigureCategory, FigureSetIdsMessageEvent, GetWardrobeMessageComposer, IAvatarFigureContainer, UserFigureComposer, UserWardrobePageEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react'; import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { GetAvatarRenderManager, GetClubMemberLevel, GetSessionDataManager, LocalizeText } from '../../api'; import { GetAvatarRenderManager, GetClubMemberLevel, GetConfiguration, GetSessionDataManager, LocalizeText } from '../../api';
import { Button } from '../../common/Button'; import { Button } from '../../common/Button';
import { ButtonGroup } from '../../common/ButtonGroup'; import { ButtonGroup } from '../../common/ButtonGroup';
import { Column } from '../../common/Column'; import { Column } from '../../common/Column';
@ -25,7 +25,6 @@ import { AvatarEditorWardrobeView } from './views/wardrobe/AvatarEditorWardrobeV
const DEFAULT_MALE_FIGURE: string = 'hr-100.hd-180-7.ch-215-66.lg-270-79.sh-305-62.ha-1002-70.wa-2007'; const DEFAULT_MALE_FIGURE: string = 'hr-100.hd-180-7.ch-215-66.lg-270-79.sh-305-62.ha-1002-70.wa-2007';
const DEFAULT_FEMALE_FIGURE: string = 'hr-515-33.hd-600-1.ch-635-70.lg-716-66-62.sh-735-68'; const DEFAULT_FEMALE_FIGURE: string = 'hr-515-33.hd-600-1.ch-635-70.lg-716-66-62.sh-735-68';
const MAX_SAVED_FIGURES: number = 10;
export const AvatarEditorView: FC<{}> = props => export const AvatarEditorView: FC<{}> = props =>
{ {
@ -36,13 +35,15 @@ export const AvatarEditorView: FC<{}> = props =>
const [ activeCategory, setActiveCategory ] = useState<IAvatarEditorCategoryModel>(null); const [ activeCategory, setActiveCategory ] = useState<IAvatarEditorCategoryModel>(null);
const [ figureSetIds, setFigureSetIds ] = useState<number[]>([]); const [ figureSetIds, setFigureSetIds ] = useState<number[]>([]);
const [ boundFurnitureNames, setBoundFurnitureNames ] = useState<string[]>([]); const [ boundFurnitureNames, setBoundFurnitureNames ] = useState<string[]>([]);
const [ savedFigures, setSavedFigures ] = useState<[ IAvatarFigureContainer, string ][]>(new Array(MAX_SAVED_FIGURES)); const [ savedFigures, setSavedFigures ] = useState<[ IAvatarFigureContainer, string ][]>([]);
const [ isWardrobeVisible, setIsWardrobeVisible ] = useState(false); const [ isWardrobeVisible, setIsWardrobeVisible ] = useState(false);
const [ lastFigure, setLastFigure ] = useState<string>(null); const [ lastFigure, setLastFigure ] = useState<string>(null);
const [ lastGender, setLastGender ] = useState<string>(null); const [ lastGender, setLastGender ] = useState<string>(null);
const [ needsReset, setNeedsReset ] = useState(false); const [ needsReset, setNeedsReset ] = useState(false);
const [ isInitalized, setIsInitalized ] = useState(false); const [ isInitalized, setIsInitalized ] = useState(false);
const maxWardrobeSlots = useMemo(() => GetConfiguration<number>('avatar.wardrobe.max.slots', 10), []);
const onAvatarEditorEvent = useCallback((event: AvatarEditorEvent) => const onAvatarEditorEvent = useCallback((event: AvatarEditorEvent) =>
{ {
switch(event.type) switch(event.type)
@ -88,7 +89,7 @@ export const AvatarEditorView: FC<{}> = props =>
let i = 0; let i = 0;
while(i < MAX_SAVED_FIGURES) while(i < maxWardrobeSlots)
{ {
savedFigures.push([ null, null ]); savedFigures.push([ null, null ]);
@ -103,7 +104,7 @@ export const AvatarEditorView: FC<{}> = props =>
} }
setSavedFigures(savedFigures) setSavedFigures(savedFigures)
}, []); }, [ maxWardrobeSlots ]);
CreateMessageHook(UserWardrobePageEvent, onUserWardrobePageEvent); CreateMessageHook(UserWardrobePageEvent, onUserWardrobePageEvent);
@ -194,6 +195,11 @@ export const AvatarEditorView: FC<{}> = props =>
setFigureData(figures.get(gender)); setFigureData(figures.get(gender));
}, [ figures ]); }, [ figures ]);
useEffect(() =>
{
setSavedFigures(new Array(maxWardrobeSlots));
}, [ maxWardrobeSlots ]);
useEffect(() => useEffect(() =>
{ {
if(!isWardrobeVisible) return; if(!isWardrobeVisible) return;

View File

@ -26,7 +26,7 @@ export const AvatarEditorPaletteSetItem: FC<AvatarEditorPaletteSetItemProps> = p
}); });
return ( return (
<LayoutGridItem itemColor={ colorItem.color } itemActive={ colorItem.isSelected } { ...rest }> <LayoutGridItem itemHighlight itemColor={ colorItem.color } itemActive={ colorItem.isSelected } { ...rest }>
{ colorItem.isHC && <CurrencyIcon className="position-absolute end-1 bottom-1" type={ 'hc' } /> } { colorItem.isHC && <CurrencyIcon className="position-absolute end-1 bottom-1" type={ 'hc' } /> }
{ children } { children }
</LayoutGridItem> </LayoutGridItem>

View File

@ -27,7 +27,7 @@ export const AvatarEditorPaletteSetView: FC<AvatarEditorPaletteSetViewProps> = p
}, [ model, category, paletteSet, paletteIndex ]); }, [ model, category, paletteSet, paletteIndex ]);
return ( return (
<AutoGrid columnCount={ 4 } columnMinWidth={ 30 }> <AutoGrid gap={ 1 } columnCount={ 5 } columnMinWidth={ 30 }>
{ (paletteSet.length > 0) && paletteSet.map((item, index) => { (paletteSet.length > 0) && paletteSet.map((item, index) =>
<AvatarEditorPaletteSetItem key={ index } colorItem={ item } onClick={ event => selectColor(item) } />) } <AvatarEditorPaletteSetItem key={ index } colorItem={ item } onClick={ event => selectColor(item) } />) }
</AutoGrid> </AutoGrid>

View File

@ -44,7 +44,6 @@
} }
.nitro-catalog-navigation-grid-container { .nitro-catalog-navigation-grid-container {
border-radius: 0.25rem;
border-color: #B6BEC5 !important; border-color: #B6BEC5 !important;
background-color: #CDD3D9; background-color: #CDD3D9;
border: 2px solid; border: 2px solid;

View File

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

View File

@ -13,9 +13,9 @@ import { CatalogEvent } from '../../../../../events/catalog/CatalogEvent';
import { useUiEvent } from '../../../../../hooks'; import { useUiEvent } from '../../../../../hooks';
import { SendMessageHook } from '../../../../../hooks/messages/message-event'; import { SendMessageHook } from '../../../../../hooks/messages/message-event';
import { LoadingSpinnerView } from '../../../../../layout/loading-spinner/LoadingSpinnerView'; import { LoadingSpinnerView } from '../../../../../layout/loading-spinner/LoadingSpinnerView';
import { GetCurrencyAmount } from '../../../../../views/purse/common/CurrencyHelper';
import { GLOBAL_PURSE } from '../../../../../views/purse/PurseView';
import { CurrencyIcon } from '../../../../../views/shared/currency-icon/CurrencyIcon'; import { CurrencyIcon } from '../../../../../views/shared/currency-icon/CurrencyIcon';
import { GetCurrencyAmount } from '../../../../purse/common/CurrencyHelper';
import { GLOBAL_PURSE } from '../../../../purse/PurseView';
import { useCatalogContext } from '../../../CatalogContext'; import { useCatalogContext } from '../../../CatalogContext';
import { CatalogPurchaseState } from '../../../common/CatalogPurchaseState'; import { CatalogPurchaseState } from '../../../common/CatalogPurchaseState';
import { CatalogLayoutProps } from './CatalogLayout.types'; import { CatalogLayoutProps } from './CatalogLayout.types';

View File

@ -9,7 +9,7 @@ import { Text } from '../../../../../../common/Text';
import { BatchUpdates, CreateMessageHook, SendMessageHook } from '../../../../../../hooks'; import { BatchUpdates, CreateMessageHook, SendMessageHook } from '../../../../../../hooks';
import { NotificationAlertType } from '../../../../../../views/notification-center/common/NotificationAlertType'; import { NotificationAlertType } from '../../../../../../views/notification-center/common/NotificationAlertType';
import { NotificationUtilities } from '../../../../../../views/notification-center/common/NotificationUtilities'; import { NotificationUtilities } from '../../../../../../views/notification-center/common/NotificationUtilities';
import { GetCurrencyAmount } from '../../../../../../views/purse/common/CurrencyHelper'; import { GetCurrencyAmount } from '../../../../../purse/common/CurrencyHelper';
import { CatalogLayoutProps } from '../CatalogLayout.types'; import { CatalogLayoutProps } from '../CatalogLayout.types';
import { CatalogLayoutMarketplaceItemView, PUBLIC_OFFER } from './CatalogLayoutMarketplaceItemView'; import { CatalogLayoutMarketplaceItemView, PUBLIC_OFFER } from './CatalogLayoutMarketplaceItemView';
import { SearchFormView } from './CatalogLayoutMarketplaceSearchFormView'; import { SearchFormView } from './CatalogLayoutMarketplaceSearchFormView';

View File

@ -8,7 +8,7 @@ import { CatalogInitPurchaseEvent } from '../../../../../events/catalog/CatalogI
import { CatalogWidgetEvent } from '../../../../../events/catalog/CatalogWidgetEvent'; import { CatalogWidgetEvent } from '../../../../../events/catalog/CatalogWidgetEvent';
import { dispatchUiEvent, SendMessageHook, useUiEvent } from '../../../../../hooks'; import { dispatchUiEvent, SendMessageHook, useUiEvent } from '../../../../../hooks';
import { LoadingSpinnerView } from '../../../../../layout'; import { LoadingSpinnerView } from '../../../../../layout';
import { GetCurrencyAmount } from '../../../../../views/purse/common/CurrencyHelper'; import { GetCurrencyAmount } from '../../../../purse/common/CurrencyHelper';
import { useCatalogContext } from '../../../CatalogContext'; import { useCatalogContext } from '../../../CatalogContext';
import { CatalogPurchaseState } from '../../../common/CatalogPurchaseState'; import { CatalogPurchaseState } from '../../../common/CatalogPurchaseState';
import { Offer } from '../../../common/Offer'; import { Offer } from '../../../common/Offer';

View File

@ -0,0 +1,21 @@
import { createContext, FC, ProviderProps, useContext } from 'react';
import { IChatHistoryState } from './common/IChatHistoryState';
import { IRoomHistoryState } from './common/IRoomHistoryState';
export interface IChatHistoryContext
{
chatHistoryState: IChatHistoryState;
roomHistoryState: IRoomHistoryState;
}
const ChatHistoryContext = createContext<IChatHistoryContext>({
chatHistoryState: null,
roomHistoryState: null
});
export const ChatHistoryContextProvider: FC<ProviderProps<IChatHistoryContext>> = props =>
{
return <ChatHistoryContext.Provider value={ props.value }>{ props.children }</ChatHistoryContext.Provider>
}
export const useChatHistoryContext = () => useContext(ChatHistoryContext);

View File

@ -2,9 +2,14 @@ import { GetGuestRoomResultEvent, RoomSessionChatEvent, RoomSessionEvent } from
import { FC, useCallback, useState } from 'react'; import { FC, useCallback, useState } from 'react';
import { GetRoomSession } from '../../api'; import { GetRoomSession } from '../../api';
import { CreateMessageHook, useRoomSessionManagerEvent } from '../../hooks'; import { CreateMessageHook, useRoomSessionManagerEvent } from '../../hooks';
import { useChatHistoryContext } from './context/ChatHistoryContext'; import { useChatHistoryContext } from './ChatHistoryContext';
import { ChatEntryType, CHAT_HISTORY_MAX, IChatEntry, IRoomHistoryEntry, ROOM_HISTORY_MAX } from './context/ChatHistoryContext.types'; import { ChatEntryType } from './common/ChatEntryType';
import { currentDate } from './utils/Utilities'; import { IChatEntry } from './common/IChatEntry';
import { IRoomHistoryEntry } from './common/IRoomHistoryEntry';
import { currentDate } from './common/Utilities';
const CHAT_HISTORY_MAX = 1000;
const ROOM_HISTORY_MAX = 10;
export const ChatHistoryMessageHandler: FC<{}> = props => export const ChatHistoryMessageHandler: FC<{}> = props =>
{ {

View File

@ -0,0 +1,8 @@
.nitro-chat-history {
width: $chat-history-width;
height: $chat-history-height;
.content-area {
min-height: 200px;
}
}

View File

@ -0,0 +1,155 @@
import { ILinkEventTracker } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AutoSizer, CellMeasurer, CellMeasurerCache, List, ListRowProps, ListRowRenderer, Size } from 'react-virtualized';
import { RenderedRows } from 'react-virtualized/dist/es/List';
import { AddEventLinkTracker, LocalizeText, RemoveLinkEventTracker } from '../../api';
import { Flex, Text } from '../../common';
import { BatchUpdates } from '../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../layout';
import { ChatHistoryContextProvider } from './ChatHistoryContext';
import { ChatHistoryMessageHandler } from './ChatHistoryMessageHandler';
import { ChatEntryType } from './common/ChatEntryType';
import { ChatHistoryState } from './common/ChatHistoryState';
import { SetChatHistory } from './common/GetChatHistory';
import { RoomHistoryState } from './common/RoomHistoryState';
export const ChatHistoryView: FC<{}> = props =>
{
const [ isVisible, setIsVisible ] = useState(false);
const [ needsScroll, setNeedsScroll ] = useState(false);
const [ chatHistoryUpdateId, setChatHistoryUpdateId ] = useState(-1);
const [ roomHistoryUpdateId, setRoomHistoryUpdateId ] = useState(-1);
const [ chatHistoryState, setChatHistoryState ] = useState(new ChatHistoryState());
const [ roomHistoryState, setRoomHistoryState ] = useState(new RoomHistoryState());
const elementRef = useRef<List>(null);
const cache = useMemo(() => new CellMeasurerCache({ defaultHeight: 25, fixedWidth: true }), []);
const RowRenderer: ListRowRenderer = (props: ListRowProps) =>
{
const item = chatHistoryState.chats[props.index];
const isDark = (props.index % 2 === 0);
return (
<CellMeasurer cache={ cache } columnIndex={ 0 } key={ props.key } parent={ props.parent } rowIndex={ props.index }>
<Flex key={ props.key } style={ props.style } className="p-1" gap={ 1 }>
<Text variant="muted">{ item.timestamp }</Text>
{ (item.type === ChatEntryType.TYPE_CHAT) &&
<>
<Text pointer noWrap dangerouslySetInnerHTML={ { __html: (item.name + ':') }} />
<Text textBreak wrap grow>{ item.message }</Text>
</> }
{ (item.type === ChatEntryType.TYPE_ROOM_INFO) &&
<>
<i className="icon icon-small-room" />
<Text textBreak wrap grow>{ item.name }</Text>
</> }
</Flex>
</CellMeasurer>
);
};
const onResize = (info: Size) => cache.clearAll();
const onRowsRendered = (info: RenderedRows) =>
{
if(elementRef && elementRef.current && isVisible && needsScroll)
{
elementRef.current.scrollToRow(chatHistoryState.chats.length);
setNeedsScroll(false);
}
}
const linkReceived = useCallback((url: string) =>
{
const parts = url.split('/');
if(parts.length < 2) return;
switch(parts[1])
{
case 'show':
setIsVisible(true);
return;
case 'hide':
setIsVisible(false);
return;
case 'toggle':
setIsVisible(prevValue => !prevValue);
return;
}
}, []);
useEffect(() =>
{
const linkTracker: ILinkEventTracker = {
linkReceived,
eventUrlPrefix: 'chat-history/'
};
AddEventLinkTracker(linkTracker);
return () => RemoveLinkEventTracker(linkTracker);
}, [ linkReceived ]);
useEffect(() =>
{
const chatState = new ChatHistoryState();
const roomState = new RoomHistoryState();
SetChatHistory(chatState);
chatState.notifier = () => setChatHistoryUpdateId(prevValue => (prevValue + 1));
roomState.notifier = () => setRoomHistoryUpdateId(prevValue => (prevValue + 1));
BatchUpdates(() =>
{
setChatHistoryState(chatState);
setRoomHistoryState(roomState);
});
return () =>
{
chatState.notifier = null;
roomState.notifier = null;
};
}, []);
useEffect(() =>
{
if(elementRef && elementRef.current && isVisible) elementRef.current.scrollToRow(chatHistoryState.chats.length);
setNeedsScroll(true);
}, [ isVisible, chatHistoryState.chats, chatHistoryUpdateId ]);
return (
<ChatHistoryContextProvider value={ { chatHistoryState, roomHistoryState } }>
<ChatHistoryMessageHandler />
{ isVisible &&
<NitroCardView uniqueKey="chat-history" className="nitro-chat-history">
<NitroCardHeaderView headerText={ LocalizeText('room.chathistory.button.text') } onCloseClick={ event => setIsVisible(false) }/>
<NitroCardContentView>
<AutoSizer defaultWidth={ 300 } defaultHeight={ 200 } onResize={ onResize }>
{ ({ height, width }) =>
{
return (
<List
ref={ elementRef }
width={ width }
height={ height }
rowCount={ chatHistoryState.chats.length }
rowHeight={ cache.rowHeight }
className={ 'chat-history-list' }
rowRenderer={ RowRenderer }
onRowsRendered={ onRowsRendered }
deferredMeasurementCache={ cache } />
)
} }
</AutoSizer>
</NitroCardContentView>
</NitroCardView> }
</ChatHistoryContextProvider>
);
}

View File

@ -0,0 +1,5 @@
export class ChatEntryType
{
public static TYPE_CHAT = 1;
public static TYPE_ROOM_INFO = 2;
}

View File

@ -1,4 +1,5 @@
import { IChatEntry, IChatHistoryState } from '../context/ChatHistoryContext.types'; import { IChatEntry } from './IChatEntry';
import { IChatHistoryState } from './IChatHistoryState';
export class ChatHistoryState implements IChatHistoryState export class ChatHistoryState implements IChatHistoryState
{ {
@ -10,6 +11,11 @@ export class ChatHistoryState implements IChatHistoryState
this._chats = []; this._chats = [];
} }
public notify(): void
{
if(this._notifier) this._notifier();
}
public get chats(): IChatEntry[] public get chats(): IChatEntry[]
{ {
return this._chats; return this._chats;
@ -24,9 +30,4 @@ export class ChatHistoryState implements IChatHistoryState
{ {
this._notifier = notifier; this._notifier = notifier;
} }
notify(): void
{
if(this._notifier) this._notifier();
}
} }

View File

@ -1,4 +1,4 @@
import { IChatHistoryState } from '../context/ChatHistoryContext.types'; import { IChatHistoryState } from './IChatHistoryState';
let GLOBAL_CHATS: IChatHistoryState = null; let GLOBAL_CHATS: IChatHistoryState = null;

View File

@ -0,0 +1,12 @@
export interface IChatEntry
{
id: number;
entityId: number;
name: string;
look?: string;
message?: string;
entityType?: number;
roomId: number;
timestamp: string;
type: number;
}

View File

@ -0,0 +1,8 @@
import { IChatEntry } from './IChatEntry';
export interface IChatHistoryState
{
chats: IChatEntry[];
notifier: () => void
notify(): void;
}

View File

@ -0,0 +1,4 @@
export interface IRoomHistoryEntry {
id: number;
name: string;
}

View File

@ -0,0 +1,8 @@
import { IRoomHistoryEntry } from './IRoomHistoryEntry';
export interface IRoomHistoryState
{
roomHistory: IRoomHistoryEntry[];
notifier: () => void;
notify: () => void;
}

View File

@ -1,4 +1,5 @@
import { IRoomHistoryEntry, IRoomHistoryState } from '../context/ChatHistoryContext.types'; import { IRoomHistoryEntry } from './IRoomHistoryEntry';
import { IRoomHistoryState } from './IRoomHistoryState';
export class RoomHistoryState implements IRoomHistoryState export class RoomHistoryState implements IRoomHistoryState
{ {

View File

@ -1,6 +1,7 @@
import { IChatEntry } from '../../../views/chat-history/context/ChatHistoryContext.types'; import { IChatEntry } from '../../chat-history/common/IChatEntry';
export interface IHelpReportState { export interface IHelpReportState
{
reportedUserId: number; reportedUserId: number;
reportedChats: IChatEntry[]; reportedChats: IChatEntry[];
cfhCategory: number; cfhCategory: number;

View File

@ -2,8 +2,9 @@ import { RoomObjectType } from '@nitrots/nitro-renderer';
import { FC, useMemo, useState } from 'react'; import { FC, useMemo, useState } from 'react';
import { LocalizeText } from '../../../api'; import { LocalizeText } from '../../../api';
import { Button, Column, Flex, Grid, LayoutGridItem, Text } from '../../../common'; import { Button, Column, Flex, Grid, LayoutGridItem, Text } from '../../../common';
import { GetChatHistory } from '../../../views/chat-history/common/GetChatHistory'; import { ChatEntryType } from '../../chat-history/common/ChatEntryType';
import { ChatEntryType, IChatEntry } from '../../../views/chat-history/context/ChatHistoryContext.types'; import { GetChatHistory } from '../../chat-history/common/GetChatHistory';
import { IChatEntry } from '../../chat-history/common/IChatEntry';
import { useHelpContext } from '../HelpContext'; import { useHelpContext } from '../HelpContext';
export const SelectReportedChatsView: FC<{}> = props => export const SelectReportedChatsView: FC<{}> = props =>

View File

@ -2,8 +2,8 @@ import { RoomObjectType } from '@nitrots/nitro-renderer';
import { FC, useMemo, useState } from 'react'; import { FC, useMemo, useState } from 'react';
import { GetSessionDataManager, LocalizeText } from '../../../api'; import { GetSessionDataManager, LocalizeText } from '../../../api';
import { Button, Column, Flex, Grid, LayoutGridItem, Text } from '../../../common'; import { Button, Column, Flex, Grid, LayoutGridItem, Text } from '../../../common';
import { GetChatHistory } from '../../../views/chat-history/common/GetChatHistory'; import { ChatEntryType } from '../../chat-history/common/ChatEntryType';
import { ChatEntryType } from '../../../views/chat-history/context/ChatHistoryContext.types'; import { GetChatHistory } from '../../chat-history/common/GetChatHistory';
import { IReportedUser } from '../common/IReportedUser'; import { IReportedUser } from '../common/IReportedUser';
import { useHelpContext } from '../HelpContext'; import { useHelpContext } from '../HelpContext';

View File

@ -1,7 +1,7 @@
import { FC, useMemo, useState } from 'react'; import { FC, useMemo, useState } from 'react';
import { LocalizeText } from '../../../api'; import { LocalizeText } from '../../../api';
import { Button, Column, Flex, Text } from '../../../common'; import { Button, Column, Flex, Text } from '../../../common';
import { GetCfhCategories } from '../../../views/mod-tools/common/GetCFHCategories'; import { GetCfhCategories } from '../../mod-tools/common/GetCFHCategories';
import { useHelpContext } from '../HelpContext'; import { useHelpContext } from '../HelpContext';
export const SelectTopicView: FC<{}> = props => export const SelectTopicView: FC<{}> = props =>

View File

@ -2,10 +2,16 @@
@import './avatar-editor/AvatarEditorView'; @import './avatar-editor/AvatarEditorView';
@import './camera/CameraWidgetView'; @import './camera/CameraWidgetView';
@import './catalog/CatalogView'; @import './catalog/CatalogView';
@import './chat-history/ChatHistoryView';
@import './groups/GroupView'; @import './groups/GroupView';
@import './help/HelpView'; @import './help/HelpView';
@import './inventory/InventoryView'; @import './inventory/InventoryView';
@import './loading/LoadingView';
@import './mod-tools/ModToolsView';
@import './navigator/NavigatorView'; @import './navigator/NavigatorView';
@import './purse/PurseView';
@import './right-side/RightSideView';
@import './toolbar/ToolbarView'; @import './toolbar/ToolbarView';
@import './user-profile/UserProfileVew';
@import './user-settings/UserSettingsView'; @import './user-settings/UserSettingsView';
@import './wired/WiredView'; @import './wired/WiredView';

View File

@ -0,0 +1,37 @@
import { FC, useEffect, useState } from 'react';
import { Base, Column } from '../../common';
import { NotificationUtilities } from '../../views/notification-center/common/NotificationUtilities';
interface LoadingViewProps
{
isError: boolean;
message: string;
}
export const LoadingView: FC<LoadingViewProps> = props =>
{
const { isError = false, message = '' } = props;
const [ loadingShowing, setLoadingShowing ] = useState(false);
useEffect(() =>
{
if(!isError) return;
NotificationUtilities.simpleAlert(message, null, null, null, 'Connection Error');
}, [ isError, message ]);
useEffect(() =>
{
const timeout = setTimeout(() => setLoadingShowing(true), 500);
return () => clearTimeout(timeout);
}, []);
return (
<Column fit center position="relative" className="nitro-loading">
<Base className="connecting-duck" />
{ isError && (message && message.length) &&
<Base className="m-auto bottom-3 fs-4 text-shadow" position="absolute">{ message }</Base> }
</Column>
);
}

View File

@ -1,33 +1,33 @@
import { HabboWebTools, RoomSessionEvent } from '@nitrots/nitro-renderer'; import { HabboWebTools, RoomSessionEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react'; import { FC, useCallback, useEffect, useState } from 'react';
import { AddEventLinkTracker, GetCommunication, RemoveLinkEventTracker } from '../../api'; import { AddEventLinkTracker, GetCommunication, RemoveLinkEventTracker } from '../../api';
import { AchievementsView } from '../../components/achievements/AchievementsView'; import { Base } from '../../common';
import { AvatarEditorView } from '../../components/avatar-editor/AvatarEditorView';
import { CameraWidgetView } from '../../components/camera/CameraWidgetView';
import { CatalogView } from '../../components/catalog/CatalogView';
import { GroupsView } from '../../components/groups/GroupsView';
import { HelpView } from '../../components/help/HelpView';
import { InventoryView } from '../../components/inventory/InventoryView';
import { NavigatorView } from '../../components/navigator/NavigatorView';
import { ToolbarView } from '../../components/toolbar/ToolbarView';
import { UserSettingsView } from '../../components/user-settings/UserSettingsView';
import { WiredView } from '../../components/wired/WiredView';
import { useRoomSessionManagerEvent } from '../../hooks/events/nitro/session/room-session-manager-event'; import { useRoomSessionManagerEvent } from '../../hooks/events/nitro/session/room-session-manager-event';
import { TransitionAnimation, TransitionAnimationTypes } from '../../layout'; import { TransitionAnimation, TransitionAnimationTypes } from '../../layout';
import { CampaignView } from '../campaign/CampaignView'; import { CampaignView } from '../../views/campaign/CampaignView';
import { FloorplanEditorView } from '../../views/floorplan-editor/FloorplanEditorView';
import { FriendsView } from '../../views/friends/FriendsView';
import { HcCenterView } from '../../views/hc-center/HcCenterView';
import { HotelView } from '../../views/hotel-view/HotelView';
import { NitropediaView } from '../../views/nitropedia/NitropediaView';
import { AchievementsView } from '../achievements/AchievementsView';
import { AvatarEditorView } from '../avatar-editor/AvatarEditorView';
import { CameraWidgetView } from '../camera/CameraWidgetView';
import { CatalogView } from '../catalog/CatalogView';
import { ChatHistoryView } from '../chat-history/ChatHistoryView'; import { ChatHistoryView } from '../chat-history/ChatHistoryView';
import { FloorplanEditorView } from '../floorplan-editor/FloorplanEditorView'; import { GroupsView } from '../groups/GroupsView';
import { FriendsView } from '../friends/FriendsView'; import { HelpView } from '../help/HelpView';
import { HcCenterView } from '../hc-center/HcCenterView'; import { InventoryView } from '../inventory/InventoryView';
import { HotelView } from '../hotel-view/HotelView';
import { ModToolsView } from '../mod-tools/ModToolsView'; import { ModToolsView } from '../mod-tools/ModToolsView';
import { NitropediaView } from '../nitropedia/NitropediaView'; import { NavigatorView } from '../navigator/NavigatorView';
import { RightSideView } from '../right-side/RightSideView'; import { RightSideView } from '../right-side/RightSideView';
import { RoomHostView } from '../room-host/RoomHostView'; import { RoomHostView } from '../room-host/RoomHostView';
import { ToolbarView } from '../toolbar/ToolbarView';
import { UserProfileView } from '../user-profile/UserProfileView'; import { UserProfileView } from '../user-profile/UserProfileView';
import { MainViewProps } from './MainView.types'; import { UserSettingsView } from '../user-settings/UserSettingsView';
import { WiredView } from '../wired/WiredView';
export const MainView: FC<MainViewProps> = props => export const MainView: FC<{}> = props =>
{ {
const [ isReady, setIsReady ] = useState(false); const [ isReady, setIsReady ] = useState(false);
const [ landingViewVisible, setLandingViewVisible ] = useState(true); const [ landingViewVisible, setLandingViewVisible ] = useState(true);
@ -93,7 +93,7 @@ export const MainView: FC<MainViewProps> = props =>
}, [onLinkReceived]); }, [onLinkReceived]);
return ( return (
<div className="nitro-main"> <Base fit>
<TransitionAnimation type={ TransitionAnimationTypes.FADE_IN } inProp={ landingViewVisible } timeout={ 300 }> <TransitionAnimation type={ TransitionAnimationTypes.FADE_IN } inProp={ landingViewVisible } timeout={ 300 }>
<HotelView /> <HotelView />
</TransitionAnimation> </TransitionAnimation>
@ -118,6 +118,6 @@ export const MainView: FC<MainViewProps> = props =>
<NitropediaView /> <NitropediaView />
<HcCenterView /> <HcCenterView />
<CampaignView /> <CampaignView />
</div> </Base>
); );
} }

View File

@ -0,0 +1,20 @@
import { createContext, Dispatch, FC, ProviderProps, useContext } from 'react';
import { IModToolsAction, IModToolsState } from './reducers/ModToolsReducer';
export interface IModToolsContext
{
modToolsState: IModToolsState;
dispatchModToolsState: Dispatch<IModToolsAction>;
}
const ModToolsContext = createContext<IModToolsContext>({
modToolsState: null,
dispatchModToolsState: null
});
export const ModToolsContextProvider: FC<ProviderProps<IModToolsContext>> = props =>
{
return <ModToolsContext.Provider value={ props.value }>{ props.children }</ModToolsContext.Provider>
}
export const useModToolsContext = () => useContext(ModToolsContext);

View File

@ -7,10 +7,10 @@ import { ModToolsOpenRoomInfoEvent } from '../../events/mod-tools/ModToolsOpenRo
import { ModToolsOpenUserChatlogEvent } from '../../events/mod-tools/ModToolsOpenUserChatlogEvent'; import { ModToolsOpenUserChatlogEvent } from '../../events/mod-tools/ModToolsOpenUserChatlogEvent';
import { ModToolsOpenUserInfoEvent } from '../../events/mod-tools/ModToolsOpenUserInfoEvent'; import { ModToolsOpenUserInfoEvent } from '../../events/mod-tools/ModToolsOpenUserInfoEvent';
import { CreateMessageHook, useRoomEngineEvent, useUiEvent } from '../../hooks'; import { CreateMessageHook, useRoomEngineEvent, useUiEvent } from '../../hooks';
import { NotificationAlertType } from '../notification-center/common/NotificationAlertType'; import { NotificationAlertType } from '../../views/notification-center/common/NotificationAlertType';
import { NotificationUtilities } from '../notification-center/common/NotificationUtilities'; import { NotificationUtilities } from '../../views/notification-center/common/NotificationUtilities';
import { SetCfhCategories } from './common/GetCFHCategories'; import { SetCfhCategories } from './common/GetCFHCategories';
import { useModToolsContext } from './context/ModToolsContext'; import { useModToolsContext } from './ModToolsContext';
import { ModToolsActions } from './reducers/ModToolsReducer'; import { ModToolsActions } from './reducers/ModToolsReducer';
export const ModToolsMessageHandler: FC<{}> = props => export const ModToolsMessageHandler: FC<{}> = props =>

View File

@ -0,0 +1,93 @@
.nitro-mod-tools {
width: $nitro-mod-tools-width;
}
.nitro-mod-tools-room {
width: 240px;
.username {
color: #1E7295;
text-decoration: underline;
}
}
.nitro-mod-tools-user {
width: 350px;
height: 370px;
.username {
color: #1E7295;
text-decoration: underline;
}
.table {
color: $black;
> :not(caption) > * > * {
box-shadow: none;
border-bottom: 1px solid rgba(0, 0, 0, .2);
}
&.table-striped > tbody > tr:nth-of-type(odd) {
color: $black;
background: rgba(0, 0, 0, .05);
}
}
}
.nitro-mod-tools-user-visits {
min-width: 300px;
.user-visits {
min-height: 200px;
.roomvisits-container {
div.room-visit {
}
}
}
}
.nitro-mod-tools-chatlog {
width: 400px;
}
.nitro-mod-tools-user-visits {
width: 250px;
}
.nitro-mod-tools-tickets {
width: 400px;
height: 200px;
}
.nitro-mod-tools-handle-issue {
width: 400px;
}
.nitro-mod-tools-chatlog,
.nitro-mod-tools-user-visits {
.log-container {
min-height: 200px;
.log-entry-container {
.log-entry {
&.highlighted {
border: 1px solid $red;
}
}
&.highlighted {
border: 1px solid $red;
}
}
&:first-child {
padding-top: 0;
}
}
}

View File

@ -1,6 +1,8 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { RoomEngineObjectEvent, RoomObjectCategory } from '@nitrots/nitro-renderer'; import { RoomEngineObjectEvent, RoomObjectCategory } from '@nitrots/nitro-renderer';
import { FC, useCallback, useReducer, useState } from 'react'; import { FC, useCallback, useReducer, useState } from 'react';
import { GetRoomSession } from '../../api'; import { GetRoomSession } from '../../api';
import { Button } from '../../common';
import { ModToolsEvent } from '../../events/mod-tools/ModToolsEvent'; import { ModToolsEvent } from '../../events/mod-tools/ModToolsEvent';
import { ModToolsOpenRoomChatlogEvent } from '../../events/mod-tools/ModToolsOpenRoomChatlogEvent'; import { ModToolsOpenRoomChatlogEvent } from '../../events/mod-tools/ModToolsOpenRoomChatlogEvent';
import { ModToolsOpenRoomInfoEvent } from '../../events/mod-tools/ModToolsOpenRoomInfoEvent'; import { ModToolsOpenRoomInfoEvent } from '../../events/mod-tools/ModToolsOpenRoomInfoEvent';
@ -8,24 +10,23 @@ import { ModToolsOpenUserInfoEvent } from '../../events/mod-tools/ModToolsOpenUs
import { useRoomEngineEvent } from '../../hooks/events'; import { useRoomEngineEvent } from '../../hooks/events';
import { dispatchUiEvent, useUiEvent } from '../../hooks/events/ui/ui-event'; import { dispatchUiEvent, useUiEvent } from '../../hooks/events/ui/ui-event';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../layout'; import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../layout';
import { ModToolsContextProvider } from './context/ModToolsContext'; import { ModToolsContextProvider } from './ModToolsContext';
import { ModToolsMessageHandler } from './ModToolsMessageHandler'; import { ModToolsMessageHandler } from './ModToolsMessageHandler';
import { ModToolsViewProps } from './ModToolsView.types';
import { initialModTools, ModToolsActions, ModToolsReducer } from './reducers/ModToolsReducer'; import { initialModTools, ModToolsActions, ModToolsReducer } from './reducers/ModToolsReducer';
import { ISelectedUser } from './utils/ISelectedUser'; import { ISelectedUser } from './utils/ISelectedUser';
import { ModToolsChatlogView } from './views/room/room-chatlog/ModToolsChatlogView'; import { ModToolsChatlogView } from './views/room/ModToolsChatlogView';
import { ModToolsRoomView } from './views/room/room-tools/ModToolsRoomView'; import { ModToolsRoomView } from './views/room/ModToolsRoomView';
import { ModToolsTicketsView } from './views/tickets/ModToolsTicketsView'; import { ModToolsTicketsView } from './views/tickets/ModToolsTicketsView';
import { ModToolsUserChatlogView } from './views/user/user-chatlog/ModToolsUserChatlogView'; import { ModToolsUserChatlogView } from './views/user/ModToolsUserChatlogView';
import { ModToolsUserView } from './views/user/user-info/ModToolsUserView'; import { ModToolsUserView } from './views/user/ModToolsUserView';
export const ModToolsView: FC<ModToolsViewProps> = props => export const ModToolsView: FC<{}> = props =>
{ {
const [ isVisible, setIsVisible ] = useState(false); const [ isVisible, setIsVisible ] = useState(false);
const [ modToolsState, dispatchModToolsState ] = useReducer(ModToolsReducer, initialModTools);
const { currentRoomId = null, openRooms = null, openRoomChatlogs = null, openUserChatlogs = null, openUserInfo = null } = modToolsState;
const [ selectedUser, setSelectedUser] = useState<ISelectedUser>(null); const [ selectedUser, setSelectedUser] = useState<ISelectedUser>(null);
const [ isTicketsVisible, setIsTicketsVisible ] = useState(false); const [ isTicketsVisible, setIsTicketsVisible ] = useState(false);
const [ modToolsState, dispatchModToolsState ] = useReducer(ModToolsReducer, initialModTools);
const { currentRoomId = null, openRooms = null, openRoomChatlogs = null, openUserChatlogs = null, openUserInfo = null } = modToolsState;
const onModToolsEvent = useCallback((event: ModToolsEvent) => const onModToolsEvent = useCallback((event: ModToolsEvent) =>
{ {
@ -191,11 +192,19 @@ export const ModToolsView: FC<ModToolsViewProps> = props =>
{ isVisible && { isVisible &&
<NitroCardView uniqueKey="mod-tools" className="nitro-mod-tools" simple={ false }> <NitroCardView uniqueKey="mod-tools" className="nitro-mod-tools" simple={ false }>
<NitroCardHeaderView headerText={ 'Mod Tools' } onCloseClick={ event => setIsVisible(false) } /> <NitroCardHeaderView headerText={ 'Mod Tools' } onCloseClick={ event => setIsVisible(false) } />
<NitroCardContentView className="text-black"> <NitroCardContentView className="text-black" gap={ 1 }>
<button className="btn btn-primary btn-sm w-100 mb-2" onClick={ () => handleClick('toggle_room') } disabled={ !currentRoomId }><i className="fas fa-home"></i> Room Tool</button> <Button gap={ 1 } onClick={ event => handleClick('toggle_room') } disabled={ !currentRoomId }>
<button className="btn btn-primary btn-sm w-100 mb-2" onClick={ () => handleClick('toggle_room_chatlog') } disabled={ !currentRoomId }><i className="fas fa-comments"></i> Chatlog Tool</button> <FontAwesomeIcon icon="home" /> Room Tool
<button className="btn btn-primary btn-sm w-100 mb-2" onClick={ () => handleClick('toggle_user_info') } disabled={ !selectedUser }><i className="fas fa-user"></i> User: { selectedUser ? selectedUser.username : '' }</button> </Button>
<button className="btn btn-primary btn-sm w-100" onClick={ () => setIsTicketsVisible(value => !value) }><i className="fas fa-exclamation-circle"></i> Report Tool</button> <Button gap={ 1 } onClick={ event => handleClick('toggle_room_chatlog') } disabled={ !currentRoomId }>
<FontAwesomeIcon icon="comments" /> Chatlog Tool
</Button>
<Button gap={ 1 } onClick={ () => handleClick('toggle_user_info') } disabled={ !selectedUser }>
<FontAwesomeIcon icon="user" /> User: { selectedUser ? selectedUser.username : '' }
</Button>
<Button gap={ 1 } onClick={ () => setIsTicketsVisible(value => !value) }>
<FontAwesomeIcon icon="exclamation-circle" /> Report Tool
</Button>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> } </NitroCardView> }
{ openRooms && openRooms.map(roomId => { openRooms && openRooms.map(roomId =>

View File

@ -0,0 +1,158 @@
import { ChatRecordData, UserProfileComposer } from '@nitrots/nitro-renderer';
import { CSSProperties, FC, Key, useCallback } from 'react';
import { AutoSizer, CellMeasurer, CellMeasurerCache, List, ListRowProps } from 'react-virtualized';
import { TryVisitRoom } from '../../../../api';
import { Base, Button, Column, Flex, Grid, Text } from '../../../../common';
import { ModToolsOpenRoomInfoEvent } from '../../../../events/mod-tools/ModToolsOpenRoomInfoEvent';
import { dispatchUiEvent, SendMessageHook } from '../../../../hooks';
interface ChatlogViewProps
{
records: ChatRecordData[];
}
export const ChatlogView: FC<ChatlogViewProps> = props =>
{
const { records = null } = props;
const rowRenderer = (props: ListRowProps) =>
{
let chatlogEntry = records[0].chatlog[props.index];
return (
<CellMeasurer
cache={ cache }
columnIndex={ 0 }
key={ props.key }
parent={ props.parent }
rowIndex={ props.index }
>
<Grid key={ props.key } fullHeight={ false } style={ props.style } gap={ 1 } alignItems="center" className="log-entry py-1 border-bottom">
<Text className="g-col-2">{ chatlogEntry.timestamp }</Text>
<Text className="g-col-3" bold underline pointer onClick={ event => SendMessageHook(new UserProfileComposer(chatlogEntry.userId)) }>{ chatlogEntry.userName }</Text>
<Text textBreak wrap className="g-col-7">{ chatlogEntry.message }</Text>
</Grid>
</CellMeasurer>
);
};
const advancedRowRenderer = (props: ListRowProps) =>
{
let chatlogEntry = null;
let currentRecord: ChatRecordData = null;
let isRoomInfo = false;
let totalIndex = 0;
for(let i = 0; i < records.length; i++)
{
currentRecord = records[i];
totalIndex++; // row for room info
totalIndex = (totalIndex + currentRecord.chatlog.length);
if(props.index > (totalIndex - 1)) continue;
if((props.index + 1) === (totalIndex - currentRecord.chatlog.length))
{
isRoomInfo = true;
break;
}
const index = (props.index - (totalIndex - currentRecord.chatlog.length));
chatlogEntry = currentRecord.chatlog[index];
break;
}
return (
<CellMeasurer
cache={ cache }
columnIndex={ 0 }
key={ props.key }
parent={ props.parent }
rowIndex={ props.index }
>
{ (isRoomInfo && currentRecord) &&
<RoomInfo roomId={ currentRecord.roomId } roomName={ currentRecord.roomName } uniqueKey={ props.key } style={ props.style } /> }
{ !isRoomInfo &&
<Grid key={ props.key } style={ props.style } gap={ 1 } alignItems="center" className="log-entry py-1 border-bottom">
<Text className="g-col-2">{ chatlogEntry.timestamp }</Text>
<Text className="g-col-3" bold underline pointer onClick={ event => SendMessageHook(new UserProfileComposer(chatlogEntry.userId)) }>{ chatlogEntry.userName }</Text>
<Text textBreak wrap className="g-col-7">{ chatlogEntry.message }</Text>
</Grid> }
</CellMeasurer>
);
}
const getNumRowsForAdvanced = useCallback(() =>
{
let count = 0;
for(let i = 0; i < records.length; i++)
{
count++; // add room info row
count = count + records[i].chatlog.length;
}
return count;
}, [records]);
const RoomInfo = (props: { roomId: number, roomName: string, uniqueKey: Key, style: CSSProperties }) =>
{
return (
<Flex key={ props.uniqueKey } gap={ 2 } alignItems="center" justifyContent="between" className="room-info bg-muted rounded p-1" style={ props.style }>
<Flex gap={ 1 }>
<Text bold>Room name:</Text>
<Text>{ props.roomName }</Text>
</Flex>
<Flex gap={ 1 }>
<Button onClick={ event => TryVisitRoom(props.roomId) }>Visit Room</Button>
<Button onClick={ event => dispatchUiEvent(new ModToolsOpenRoomInfoEvent(props.roomId)) }>Room Tools</Button>
</Flex>
</Flex>
);
}
const cache = new CellMeasurerCache({
defaultHeight: 25,
fixedWidth: true
});
return (
<>
{ (records && (records.length === 1)) &&
<RoomInfo roomId={records[0].roomId} roomName={records[0].roomName} uniqueKey={ null } style={ {} } /> }
<Column fit gap={ 0 } overflow="hidden">
<Column gap={ 2 }>
<Grid gap={ 1 } className="text-black fw-bold border-bottom pb-1">
<Base className="g-col-2">Time</Base>
<Base className="g-col-3">User</Base>
<Base className="g-col-7">Message</Base>
</Grid>
</Column>
{ (records && (records.length > 0)) &&
<Column className="log-container striped-children" overflow="auto" gap={ 0 }>
<AutoSizer defaultWidth={ 400 } defaultHeight={ 200 }>
{ ({ height, width }) =>
{
cache.clearAll();
return (
<List
width={ width }
height={ height }
rowCount={ (records.length > 1) ? getNumRowsForAdvanced() : records[0].chatlog.length }
rowHeight={ cache.rowHeight }
className={ 'log-entry-container' }
rowRenderer={ (records.length > 1) ? advancedRowRenderer : rowRenderer }
deferredMeasurementCache={ cache } />
);
} }
</AutoSizer>
</Column> }
</Column>
</>
);
}

View File

@ -1,20 +1,19 @@
import { ChatRecordData, GetRoomChatlogMessageComposer, RoomChatlogEvent } from '@nitrots/nitro-renderer'; import { ChatRecordData, GetRoomChatlogMessageComposer, RoomChatlogEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react'; import { FC, useCallback, useEffect, useState } from 'react';
import { CreateMessageHook, SendMessageHook } from '../../../../../hooks/messages'; import { CreateMessageHook, SendMessageHook } from '../../../../hooks/messages';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout'; import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { ChatlogView } from '../../chatlog/ChatlogView'; import { ChatlogView } from '../chatlog/ChatlogView';
import { ModToolsChatlogViewProps } from './ModToolsChatlogView.types';
interface ModToolsChatlogViewProps
{
roomId: number;
onCloseClick: () => void;
}
export const ModToolsChatlogView: FC<ModToolsChatlogViewProps> = props => export const ModToolsChatlogView: FC<ModToolsChatlogViewProps> = props =>
{ {
const { roomId = null, onCloseClick = null } = props; const { roomId = null, onCloseClick = null } = props;
const [ roomChatlog, setRoomChatlog ] = useState<ChatRecordData>(null);
const [roomChatlog, setRoomChatlog] = useState<ChatRecordData>(null);
useEffect(() =>
{
SendMessageHook(new GetRoomChatlogMessageComposer(roomId));
}, [roomId]);
const onModtoolRoomChatlogEvent = useCallback((event: RoomChatlogEvent) => const onModtoolRoomChatlogEvent = useCallback((event: RoomChatlogEvent) =>
{ {
@ -23,17 +22,23 @@ export const ModToolsChatlogView: FC<ModToolsChatlogViewProps> = props =>
if(!parser || parser.data.roomId !== roomId) return; if(!parser || parser.data.roomId !== roomId) return;
setRoomChatlog(parser.data); setRoomChatlog(parser.data);
}, [roomId, setRoomChatlog]); }, [ roomId ]);
CreateMessageHook(RoomChatlogEvent, onModtoolRoomChatlogEvent); CreateMessageHook(RoomChatlogEvent, onModtoolRoomChatlogEvent);
useEffect(() =>
{
SendMessageHook(new GetRoomChatlogMessageComposer(roomId));
}, [ roomId ]);
if(!roomChatlog) return null;
return ( return (
<NitroCardView className="nitro-mod-tools-room-chatlog" simple={true}> <NitroCardView className="nitro-mod-tools-chatlog" simple={true}>
<NitroCardHeaderView headerText={'Room Chatlog' + (roomChatlog ? ': ' + roomChatlog.roomName : '')} onCloseClick={() => onCloseClick()} /> <NitroCardHeaderView headerText={ `Room Chatlog ${ roomChatlog.roomName }` } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black h-100"> <NitroCardContentView className="text-black h-100">
{roomChatlog && { roomChatlog &&
<ChatlogView records={[roomChatlog]} /> <ChatlogView records={ [ roomChatlog ] } /> }
}
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
); );

View File

@ -0,0 +1,128 @@
import { GetModeratorRoomInfoMessageComposer, ModerateRoomMessageComposer, ModeratorActionMessageComposer, ModeratorRoomInfoEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { TryVisitRoom } from '../../../../api';
import { Button, Column, Flex, Text } from '../../../../common';
import { ModToolsOpenRoomChatlogEvent } from '../../../../events/mod-tools/ModToolsOpenRoomChatlogEvent';
import { BatchUpdates, dispatchUiEvent } from '../../../../hooks';
import { CreateMessageHook, SendMessageHook } from '../../../../hooks/messages';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
interface ModToolsRoomViewProps
{
roomId: number;
onCloseClick: () => void;
}
export const ModToolsRoomView: FC<ModToolsRoomViewProps> = props =>
{
const { roomId = null, onCloseClick = null } = props;
const [ infoRequested, setInfoRequested ] = useState(false);
const [ loadedRoomId, setLoadedRoomId ] = useState(null);
const [ name, setName ] = useState(null);
const [ ownerId, setOwnerId ] = useState(null);
const [ ownerName, setOwnerName ] = useState(null);
const [ ownerInRoom, setOwnerInRoom ] = useState(false);
const [ usersInRoom, setUsersInRoom ] = useState(0);
//form data
const [ kickUsers, setKickUsers ] = useState(false);
const [ lockRoom, setLockRoom ] = useState(false);
const [ changeRoomName, setChangeRoomName ] = useState(false);
const [ message, setMessage ] = useState('');
const onModtoolRoomInfoEvent = useCallback((event: ModeratorRoomInfoEvent) =>
{
const parser = event.getParser();
if(!parser || parser.data.flatId !== roomId) return;
BatchUpdates(() =>
{
setLoadedRoomId(parser.data.flatId);
setName(parser.data.room.name);
setOwnerId(parser.data.ownerId);
setOwnerName(parser.data.ownerName);
setOwnerInRoom(parser.data.ownerInRoom);
setUsersInRoom(parser.data.userCount);
});
}, [ roomId ]);
CreateMessageHook(ModeratorRoomInfoEvent, onModtoolRoomInfoEvent);
const handleClick = useCallback((action: string, value?: string) =>
{
if(!action) return;
switch(action)
{
case 'alert_only':
if(message.trim().length === 0) return;
SendMessageHook(new ModeratorActionMessageComposer(ModeratorActionMessageComposer.ACTION_ALERT, message, ''));
return;
case 'send_message':
if(message.trim().length === 0) return;
SendMessageHook(new ModeratorActionMessageComposer(ModeratorActionMessageComposer.ACTION_MESSAGE, message, ''));
SendMessageHook(new ModerateRoomMessageComposer(roomId, lockRoom ? 1 : 0, changeRoomName ? 1 : 0, kickUsers ? 1 : 0))
return;
}
}, [ changeRoomName, kickUsers, lockRoom, message, roomId ]);
useEffect(() =>
{
if(infoRequested) return;
SendMessageHook(new GetModeratorRoomInfoMessageComposer(roomId));
setInfoRequested(true);
}, [ roomId, infoRequested, setInfoRequested ]);
return (
<NitroCardView className="nitro-mod-tools-room" simple>
<NitroCardHeaderView headerText={ 'Room Info' + (name ? ': ' + name : '') } onCloseClick={ event => onCloseClick() } />
<NitroCardContentView className="text-black">
<Flex gap={ 2 }>
<Column justifyContent="center" grow gap={ 1 }>
<Flex alignItems="center" gap={ 2 }>
<Text bold align="end" className="col-7">Room Owner:</Text>
<Text underline pointer truncate>{ ownerName }</Text>
</Flex>
<Flex alignItems="center" gap={ 2 }>
<Text bold align="end" className="col-7">Users in room:</Text>
<Text>{ usersInRoom }</Text>
</Flex>
<Flex alignItems="center" gap={ 2 }>
<Text bold align="end" className="col-7">Owner in room:</Text>
<Text>{ ownerInRoom ? 'Yes' : 'No' }</Text>
</Flex>
</Column>
<Column gap={ 1 }>
<Button onClick={ event => TryVisitRoom(roomId) }>Visit Room</Button>
<Button onClick={ event => dispatchUiEvent(new ModToolsOpenRoomChatlogEvent(roomId)) }>Chatlog</Button>
</Column>
</Flex>
<Column className="bg-muted rounded p-2" gap={ 1 }>
<Flex alignItems="center" gap={ 1 }>
<input className="form-check-input" type="checkbox" checked={ kickUsers } onChange={ event => setKickUsers(event.target.checked) } />
<Text small>Kick everyone out</Text>
</Flex>
<Flex alignItems="center" gap={ 1 }>
<input className="form-check-input" type="checkbox" checked={ lockRoom } onChange={ event => setLockRoom(event.target.checked) } />
<Text small>Enable the doorbell</Text>
</Flex>
<Flex alignItems="center" gap={ 1 }>
<input className="form-check-input" type="checkbox" checked={ changeRoomName } onChange={ event => setChangeRoomName(event.target.checked) }/>
<Text small>Change room name</Text>
</Flex>
</Column>
<textarea className="form-control" placeholder="Type a mandatory message to the users in this text box..." value={ message } onChange={ event => setMessage(event.target.value) }></textarea>
<Flex justifyContent="between">
<Button variant="danger" onClick={ event => handleClick('send_message') }>Send Caution</Button>
<Button onClick={ event => handleClick('alert_only') }>Send Alert only</Button>
</Flex>
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -1,9 +1,14 @@
import { CfhChatlogData, CfhChatlogEvent, GetCfhChatlogMessageComposer } from '@nitrots/nitro-renderer'; import { CfhChatlogData, CfhChatlogEvent, GetCfhChatlogMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react'; import { FC, useCallback, useEffect, useState } from 'react';
import { CreateMessageHook, SendMessageHook } from '../../../../../hooks'; import { CreateMessageHook, SendMessageHook } from '../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout'; import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { ChatlogView } from '../../chatlog/ChatlogView'; import { ChatlogView } from '../chatlog/ChatlogView';
import { CfhChatlogViewProps } from './CfhChatlogView.types';
interface CfhChatlogViewProps
{
issueId: number;
onCloseClick(): void;
}
export const CfhChatlogView: FC<CfhChatlogViewProps> = props => export const CfhChatlogView: FC<CfhChatlogViewProps> = props =>
{ {

View File

@ -0,0 +1,99 @@
import { CloseIssuesMessageComposer, ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useMemo, useState } from 'react';
import { LocalizeText } from '../../../../api';
import { Button, Column, Grid, Text } from '../../../../common';
import { ModToolsOpenUserInfoEvent } from '../../../../events/mod-tools/ModToolsOpenUserInfoEvent';
import { dispatchUiEvent, SendMessageHook } from '../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { getSourceName } from '../../common/IssueCategoryNames';
import { useModToolsContext } from '../../ModToolsContext';
import { CfhChatlogView } from './CfhChatlogView';
interface IssueInfoViewProps
{
issueId: number;
onIssueInfoClosed(issueId: number): void;
}
export const ModToolsIssueInfoView: FC<IssueInfoViewProps> = props =>
{
const { issueId = null, onIssueInfoClosed = null } = props;
const { modToolsState = null } = useModToolsContext();
const { tickets = null } = modToolsState;
const [ cfhChatlogOpen, setcfhChatlogOpen ] = useState(false);
const ticket = useMemo(() =>
{
if(!tickets || !tickets.length) return null;
return tickets.find(issue => issue.issueId === issueId);
}, [ issueId, tickets ]);
const releaseIssue = (issueId: number) =>
{
SendMessageHook(new ReleaseIssuesMessageComposer([ issueId ]));
onIssueInfoClosed(issueId);
}
const closeIssue = (resolutionType: number) =>
{
SendMessageHook(new CloseIssuesMessageComposer([ issueId ], resolutionType));
onIssueInfoClosed(issueId)
}
const openUserInfo = (userId: number) => dispatchUiEvent(new ModToolsOpenUserInfoEvent(userId));
return (
<>
<NitroCardView className="nitro-mod-tools-handle-issue" simple>
<NitroCardHeaderView headerText={'Resolving issue ' + issueId} onCloseClick={() => onIssueInfoClosed(issueId)} />
<NitroCardContentView className="text-black">
<Text fontSize={ 4 }>Issue Information</Text>
<Grid>
<Column size={ 8 }>
<table className="table table-striped table-sm table-text-small text-black m-0">
<tbody>
<tr>
<th>Source</th>
<td>{ getSourceName(ticket.categoryId) }</td>
</tr>
<tr>
<th>Category</th>
<td>{ LocalizeText('help.cfh.topic.' + ticket.reportedCategoryId) }</td>
</tr>
<tr>
<th>Description</th>
<td>{ ticket.message }</td>
</tr>
<tr>
<th>Caller</th>
<td>
<Text bold underline pointer onClick={ event => openUserInfo(ticket.reporterUserId) }>{ ticket.reporterUserName }</Text>
</td>
</tr>
<tr>
<th>Reported User</th>
<td>
<Text bold underline pointer onClick={ event => openUserInfo(ticket.reportedUserId) }>{ ticket.reportedUserName }</Text>
</td>
</tr>
</tbody>
</table>
</Column>
<Column size={ 4 } gap={ 1 }>
<Button variant="secondary" onClick={ () => setcfhChatlogOpen(!cfhChatlogOpen) }>Chatlog</Button>
<Button onClick={ event => closeIssue(CloseIssuesMessageComposer.RESOLUTION_USELESS) }>Close as useless</Button>
<Button variant="danger" onClick={ event => closeIssue(CloseIssuesMessageComposer.RESOLUTION_ABUSIVE) }>Close as abusive</Button>
<Button variant="success" onClick={ event => closeIssue(CloseIssuesMessageComposer.RESOLUTION_RESOLVED) }>Close as resolved</Button>
<Button variant="secondary" onClick={ event => releaseIssue(issueId)} >Release</Button>
</Column>
</Grid>
</NitroCardContentView>
</NitroCardView>
{ cfhChatlogOpen &&
<CfhChatlogView issueId={ issueId } onCloseClick={ () => setcfhChatlogOpen(false) }/> }
</>
);
}

View File

@ -0,0 +1,49 @@
import { IssueMessageData, ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { Base, Button, Column, Grid } from '../../../../common';
import { SendMessageHook } from '../../../../hooks';
interface ModToolsMyIssuesTabViewProps
{
myIssues: IssueMessageData[];
onIssueHandleClick(issueId: number): void;
}
export const ModToolsMyIssuesTabView: FC<ModToolsMyIssuesTabViewProps> = props =>
{
const { myIssues = null, onIssueHandleClick = null } = props;
const onReleaseIssue = (issueId: number) => SendMessageHook(new ReleaseIssuesMessageComposer([issueId]));
return (
<Column gap={ 0 } overflow="hidden">
<Column gap={ 2 }>
<Grid gap={ 1 } className="text-black fw-bold border-bottom pb-1">
<Base className="g-col-2">Type</Base>
<Base className="g-col-3">Room/Player</Base>
<Base className="g-col-3">Opened</Base>
<Base className="g-col-2"></Base>
<Base className="g-col-2"></Base>
</Grid>
</Column>
<Column overflow="auto" className="striped-children" gap={ 0 }>
{ myIssues && (myIssues.length > 0) && myIssues.map(issue =>
{
return (
<Grid key={ issue.issueId } gap={ 1 } alignItems="center" className="text-black py-1 border-bottom">
<Base className="g-col-2">{ issue.categoryId }</Base>
<Base className="g-col-3">{ issue.reportedUserName }</Base>
<Base className="g-col-3">{ new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() }</Base>
<Base className="g-col-2">
<Button variant="primary" onClick={ event => onIssueHandleClick(issue.issueId) }>Handle</Button>
</Base>
<Base className="g-col-2">
<Button variant="danger" onClick={ event => onReleaseIssue(issue.issueId) }>Release</Button>
</Base>
</Grid>
);
}) }
</Column>
</Column>
);
}

View File

@ -0,0 +1,44 @@
import { IssueMessageData, PickIssuesMessageComposer } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { Base, Button, Column, Grid } from '../../../../common';
import { SendMessageHook } from '../../../../hooks';
interface ModToolsOpenIssuesTabViewProps
{
openIssues: IssueMessageData[];
}
export const ModToolsOpenIssuesTabView: FC<ModToolsOpenIssuesTabViewProps> = props =>
{
const { openIssues = null } = props;
const onPickIssue = (issueId: number) => SendMessageHook(new PickIssuesMessageComposer([issueId], false, 0, 'pick issue button'));
return (
<Column gap={ 0 } overflow="hidden">
<Column gap={ 2 }>
<Grid gap={ 1 } className="text-black fw-bold border-bottom pb-1">
<Base className="g-col-2">Type</Base>
<Base className="g-col-3">Room/Player</Base>
<Base className="g-col-4">Opened</Base>
<Base className="g-col-3"></Base>
</Grid>
</Column>
<Column overflow="auto" className="striped-children" gap={ 0 }>
{ openIssues && (openIssues.length > 0) && openIssues.map(issue =>
{
return (
<Grid key={ issue.issueId } gap={ 1 } alignItems="center" className="text-black py-1 border-bottom">
<Base className="g-col-2">{ issue.categoryId }</Base>
<Base className="g-col-3">{ issue.reportedUserName }</Base>
<Base className="g-col-4">{ new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() }</Base>
<Base className="g-col-3">
<Button variant="success" onClick={ event => onPickIssue(issue.issueId) }>Pick Issue</Button>
</Base>
</Grid>
);
}) }
</Column>
</Column>
);
}

View File

@ -0,0 +1,39 @@
import { IssueMessageData } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { Base, Column, Grid } from '../../../../common';
interface ModToolsPickedIssuesTabViewProps
{
pickedIssues: IssueMessageData[];
}
export const ModToolsPickedIssuesTabView: FC<ModToolsPickedIssuesTabViewProps> = props =>
{
const { pickedIssues = null } = props;
return (
<Column gap={ 0 } overflow="hidden">
<Column gap={ 2 }>
<Grid gap={ 1 } className="text-black fw-bold border-bottom pb-1">
<Base className="g-col-2">Type</Base>
<Base className="g-col-3">Room/Player</Base>
<Base className="g-col-4">Opened</Base>
<Base className="g-col-3">Picker</Base>
</Grid>
</Column>
<Column overflow="auto" className="striped-children" gap={ 0 }>
{ pickedIssues && (pickedIssues.length > 0) && pickedIssues.map(issue =>
{
return (
<Grid key={ issue.issueId } gap={ 1 } alignItems="center" className="text-black py-1 border-bottom">
<Base className="g-col-2">{ issue.categoryId }</Base>
<Base className="g-col-3">{ issue.reportedUserName }</Base>
<Base className="g-col-4">{ new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() }</Base>
<Base className="g-col-3">{ issue.pickerUserName }</Base>
</Grid>
);
}) }
</Column>
</Column>
);
}

View File

@ -2,12 +2,16 @@ import { IssueMessageData } from '@nitrots/nitro-renderer';
import { FC, useCallback, useMemo, useState } from 'react'; import { FC, useCallback, useMemo, useState } from 'react';
import { GetSessionDataManager } from '../../../../api'; import { GetSessionDataManager } from '../../../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../layout'; import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../layout';
import { useModToolsContext } from '../../context/ModToolsContext'; import { useModToolsContext } from '../../ModToolsContext';
import { IssueInfoView } from './issue-info/IssueInfoView'; import { ModToolsIssueInfoView } from './ModToolsIssueInfoView';
import { ModToolsTicketsViewProps } from './ModToolsTicketsView.types'; import { ModToolsMyIssuesTabView } from './ModToolsMyIssuesTabView';
import { ModToolsMyIssuesTabView } from './my-issues/ModToolsMyIssuesTabView'; import { ModToolsOpenIssuesTabView } from './ModToolsOpenIssuesTabView';
import { ModToolsOpenIssuesTabView } from './open-issues/ModToolsOpenIssuesTabView'; import { ModToolsPickedIssuesTabView } from './ModToolsPickedIssuesTabView';
import { ModToolsPickedIssuesTabView } from './picked-issues/ModToolsPickedIssuesTabView';
interface ModToolsTicketsViewProps
{
onCloseClick: () => void;
}
const TABS: string[] = [ const TABS: string[] = [
'Open Issues', 'Open Issues',
@ -82,25 +86,21 @@ export const ModToolsTicketsView: FC<ModToolsTicketsViewProps> = props =>
return ( return (
<> <>
<NitroCardView className="nitro-mod-tools-tickets" simple={ false }> <NitroCardView className="nitro-mod-tools-tickets">
<NitroCardHeaderView headerText={ 'Tickets' } onCloseClick={ onCloseClick } /> <NitroCardHeaderView headerText={ 'Tickets' } onCloseClick={ onCloseClick } />
<NitroCardContentView className="p-0 text-black">
<NitroCardTabsView> <NitroCardTabsView>
{ TABS.map((tab, index) => { TABS.map((tab, index) =>
{ {
return (<NitroCardTabsItemView key={ index } isActive={ currentTab === index } onClick={ () => setCurrentTab(index) }> return (<NitroCardTabsItemView key={ index } isActive={ (currentTab === index) } onClick={ event => setCurrentTab(index) }>
{ tab } { tab }
</NitroCardTabsItemView>); </NitroCardTabsItemView>);
}) } }) }
</NitroCardTabsView> </NitroCardTabsView>
<div className="p-2"> <NitroCardContentView gap={ 1 }>
<CurrentTabComponent /> <CurrentTabComponent />
</div>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
{ { issueInfoWindows && (issueInfoWindows.length > 0) && issueInfoWindows.map(issueId => <ModToolsIssueInfoView key={ issueId } issueId={ issueId } onIssueInfoClosed={ onIssueInfoClosed } />) }
issueInfoWindows && issueInfoWindows.map(issueId => <IssueInfoView key={issueId} issueId={issueId} onIssueInfoClosed={onIssueInfoClosed}/>)
}
</> </>
); );
} }

View File

@ -1,20 +1,20 @@
import { ChatRecordData, GetUserChatlogMessageComposer, UserChatlogEvent } from '@nitrots/nitro-renderer'; import { ChatRecordData, GetUserChatlogMessageComposer, UserChatlogEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react'; import { FC, useCallback, useEffect, useState } from 'react';
import { CreateMessageHook, SendMessageHook } from '../../../../../hooks'; import { BatchUpdates, CreateMessageHook, SendMessageHook } from '../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout'; import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { ChatlogView } from '../../chatlog/ChatlogView'; import { ChatlogView } from '../chatlog/ChatlogView';
import { ModToolsUserChatlogViewProps } from './ModToolsUserChatlogView.types';
interface ModToolsUserChatlogViewProps
{
userId: number;
onCloseClick: () => void;
}
export const ModToolsUserChatlogView: FC<ModToolsUserChatlogViewProps> = props => export const ModToolsUserChatlogView: FC<ModToolsUserChatlogViewProps> = props =>
{ {
const { userId = null, onCloseClick = null } = props; const { userId = null, onCloseClick = null } = props;
const [userChatlog, setUserChatlog] = useState<ChatRecordData[]>(null); const [ userChatlog, setUserChatlog ] = useState<ChatRecordData[]>(null);
const [username, setUsername] = useState<string>(null); const [ username, setUsername ] = useState<string>(null);
useEffect(() =>
{
SendMessageHook(new GetUserChatlogMessageComposer(userId));
}, [userId]);
const onModtoolUserChatlogEvent = useCallback((event: UserChatlogEvent) => const onModtoolUserChatlogEvent = useCallback((event: UserChatlogEvent) =>
{ {
@ -22,19 +22,26 @@ export const ModToolsUserChatlogView: FC<ModToolsUserChatlogViewProps> = props =
if(!parser || parser.data.userId !== userId) return; if(!parser || parser.data.userId !== userId) return;
BatchUpdates(() =>
{
setUsername(parser.data.username); setUsername(parser.data.username);
setUserChatlog(parser.data.roomChatlogs); setUserChatlog(parser.data.roomChatlogs);
}, [setUsername, setUserChatlog, userId]); });
}, [ userId ]);
CreateMessageHook(UserChatlogEvent, onModtoolUserChatlogEvent); CreateMessageHook(UserChatlogEvent, onModtoolUserChatlogEvent);
useEffect(() =>
{
SendMessageHook(new GetUserChatlogMessageComposer(userId));
}, [ userId ]);
return ( return (
<NitroCardView className="nitro-mod-tools-user-chatlog" simple={true}> <NitroCardView className="nitro-mod-tools-chatlog" simple>
<NitroCardHeaderView headerText={'User Chatlog' + (username ? ': ' + username : '')} onCloseClick={() => onCloseClick()} /> <NitroCardHeaderView headerText={ `User Chatlog: ${ username || '' }` } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black h-100"> <NitroCardContentView className="text-black h-100">
{userChatlog && { userChatlog &&
<ChatlogView records={userChatlog} /> <ChatlogView records={userChatlog} /> }
}
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
); );

View File

@ -0,0 +1,182 @@
import { CallForHelpTopicData, DefaultSanctionMessageComposer, ModAlertMessageComposer, ModBanMessageComposer, ModKickMessageComposer, ModMessageMessageComposer, ModMuteMessageComposer, ModTradingLockMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useMemo, useState } from 'react';
import { LocalizeText } from '../../../../api';
import { Button, Column, Flex, Text } from '../../../../common';
import { SendMessageHook } from '../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { NotificationAlertType } from '../../../../views/notification-center/common/NotificationAlertType';
import { NotificationUtilities } from '../../../../views/notification-center/common/NotificationUtilities';
import { useModToolsContext } from '../../ModToolsContext';
import { ISelectedUser } from '../../utils/ISelectedUser';
import { ModActionDefinition } from '../../utils/ModActionDefinition';
interface ModToolsUserModActionViewProps
{
user: ISelectedUser;
onCloseClick: () => void;
}
const MOD_ACTION_DEFINITIONS = [
new ModActionDefinition(1, 'Alert', ModActionDefinition.ALERT, 1, 0),
new ModActionDefinition(2, 'Mute 1h', ModActionDefinition.MUTE, 2, 0),
new ModActionDefinition(4, 'Ban 7 days', ModActionDefinition.BAN, 4, 0),
new ModActionDefinition(3, 'Ban 18h', ModActionDefinition.BAN, 3, 0),
new ModActionDefinition(5, 'Ban 30 days (step 1)', ModActionDefinition.BAN, 5, 0),
new ModActionDefinition(7, 'Ban 30 days (step 2)', ModActionDefinition.BAN, 7, 0),
new ModActionDefinition(6, 'Ban 100 years', ModActionDefinition.BAN, 6, 0),
new ModActionDefinition(106, 'Ban avatar-only 100 years', ModActionDefinition.BAN, 6, 0),
new ModActionDefinition(101, 'Kick', ModActionDefinition.KICK, 0, 0),
new ModActionDefinition(102, 'Lock trade 1 week', ModActionDefinition.TRADE_LOCK, 0, 168),
new ModActionDefinition(104, 'Lock trade permanent', ModActionDefinition.TRADE_LOCK, 0, 876000),
new ModActionDefinition(105, 'Message', ModActionDefinition.MESSAGE, 0, 0),
];
export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = props =>
{
const { user = null, onCloseClick = null } = props;
const [ selectedTopic, setSelectedTopic ] = useState(-1);
const [ selectedAction, setSelectedAction ] = useState(-1);
const [ message, setMessage ] = useState<string>('');
const { modToolsState = null } = useModToolsContext();
const { cfhCategories = null, settings = null } = modToolsState;
const topics = useMemo(() =>
{
const values: CallForHelpTopicData[] = [];
if(cfhCategories && cfhCategories.length)
{
for(const category of cfhCategories)
{
for(const topic of category.topics) values.push(topic);
}
}
return values;
}, [ cfhCategories ]);
const sendAlert = (message: string) =>
{
NotificationUtilities.simpleAlert(message, NotificationAlertType.DEFAULT, null, null, 'Error');
}
const sendDefaultSanction = () =>
{
SendMessageHook(new DefaultSanctionMessageComposer(user.userId, selectedTopic, message));
onCloseClick();
}
const sendSanction = () =>
{
let errorMessage: string = null;
const category = topics[selectedTopic];
const sanction = MOD_ACTION_DEFINITIONS[selectedAction];
if((selectedTopic === -1) || (selectedAction === -1)) errorMessage = 'You must select a CFH topic and Sanction';
else if(!settings || !settings.cfhPermission) errorMessage = 'You do not have permission to do this';
else if(!category) errorMessage = 'You must select a CFH topic';
else if(!sanction) errorMessage = 'You must select a sanction';
if(errorMessage)
{
sendAlert('You must select a sanction');
return;
}
const messageOrDefault = (message.trim().length === 0) ? LocalizeText(`help.cfh.topic.${ category.id }`) : message;
switch(sanction.actionType)
{
case ModActionDefinition.ALERT: {
if(!settings.alertPermission)
{
sendAlert('You have insufficient permissions');
return;
}
if(message.trim().length === 0)
{
sendAlert('Please write a message to user');
return;
}
SendMessageHook(new ModAlertMessageComposer(user.userId, message, category.id));
break;
}
case ModActionDefinition.MUTE:
SendMessageHook(new ModMuteMessageComposer(user.userId, messageOrDefault, category.id));
break;
case ModActionDefinition.BAN: {
if(!settings.banPermission)
{
sendAlert('You have insufficient permissions');
return;
}
SendMessageHook(new ModBanMessageComposer(user.userId, messageOrDefault, category.id, selectedAction, (sanction.actionId === 106)));
break;
}
case ModActionDefinition.KICK: {
if(!settings.kickPermission)
{
sendAlert('You have insufficient permissions');
return;
}
SendMessageHook(new ModKickMessageComposer(user.userId, messageOrDefault, category.id));
break;
}
case ModActionDefinition.TRADE_LOCK: {
const numSeconds = (sanction.actionLengthHours * 60);
SendMessageHook(new ModTradingLockMessageComposer(user.userId, messageOrDefault, numSeconds, category.id));
break;
}
case ModActionDefinition.MESSAGE: {
if(message.trim().length === 0)
{
sendAlert('Please write a message to user');
return;
}
SendMessageHook(new ModMessageMessageComposer(user.userId, message, category.id));
break;
}
}
onCloseClick();
}
if(!user) return null;
return (
<NitroCardView className="nitro-mod-tools-user-action" simple={true}>
<NitroCardHeaderView headerText={'Mod Action: ' + (user ? user.username : '')} onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black">
<select className="form-select form-select-sm" value={ selectedTopic } onChange={ event => setSelectedTopic(parseInt(event.target.value)) }>
<option value={ -1 } disabled>CFH Topic</option>
{ topics.map((topic, index) => <option key={ index } value={ index }>{LocalizeText('help.cfh.topic.' + topic.id)}</option>) }
</select>
<select className="form-select form-select-sm" value={ selectedAction } onChange={ event => setSelectedAction(parseInt(event.target.value)) }>
<option value={ -1 } disabled>Sanction Type</option>
{ MOD_ACTION_DEFINITIONS.map((action, index) => <option key={ index } value={ index }>{ action.name }</option>) }
</select>
<Column gap={ 1 }>
<Text small>Optional message type, overrides default</Text>
<textarea className="form-control" value={ message } onChange={ event => setMessage(event.target.value) }/>
</Column>
<Flex justifyContent="between" gap={ 1 }>
<Button variant="danger" onClick={ sendSanction }>Sanction</Button>
<Button variant="success" onClick={ sendDefaultSanction }>Default Sanction</Button>
</Flex>
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -0,0 +1,85 @@
import { GetRoomVisitsMessageComposer, RoomVisitsData, RoomVisitsEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { AutoSizer, List, ListRowProps } from 'react-virtualized';
import { TryVisitRoom } from '../../../../api';
import { Base, Column, Grid, Text } from '../../../../common';
import { CreateMessageHook, SendMessageHook } from '../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
interface ModToolsUserRoomVisitsViewProps
{
userId: number;
onCloseClick: () => void;
}
export const ModToolsUserRoomVisitsView: FC<ModToolsUserRoomVisitsViewProps> = props =>
{
const { userId = null, onCloseClick = null } = props;
const [ roomVisitData, setRoomVisitData ] = useState<RoomVisitsData>(null);
const onModtoolReceivedRoomsUserEvent = useCallback((event: RoomVisitsEvent) =>
{
const parser = event.getParser();
if(!parser || (parser.data.userId !== userId)) return;
setRoomVisitData(parser.data);
}, [ userId ]);
CreateMessageHook(RoomVisitsEvent, onModtoolReceivedRoomsUserEvent);
const RowRenderer = (props: ListRowProps) =>
{
const item = roomVisitData.rooms[props.index];
return (
<Grid key={ props.key } style={ props.style } gap={ 1 } alignItems="center" className="text-black py-1 border-bottom">
<Text className="g-col-2">{ item.enterHour.toString().padStart(2, '0') }: { item.enterMinute.toString().padStart(2, '0') }</Text>
<Text className="g-col-7">{ item.roomName }</Text>
<Text bold underline pointer variant="primary" className="g-col-3" onClick={ event => TryVisitRoom(item.roomId) }>Visit Room</Text>
</Grid>
);
}
useEffect(() =>
{
SendMessageHook(new GetRoomVisitsMessageComposer(userId));
}, [userId]);
if(!userId) return null;
return (
<NitroCardView className="nitro-mod-tools-user-visits" simple>
<NitroCardHeaderView headerText={ 'User Visits' } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black" gap={ 1 }>
<Column gap={ 0 } overflow="hidden">
<Column gap={ 2 }>
<Grid gap={ 1 } className="text-black fw-bold border-bottom pb-1">
<Base className="g-col-2">Time</Base>
<Base className="g-col-7">Room name</Base>
<Base className="g-col-3">Visit</Base>
</Grid>
</Column>
<Column className="log-container striped-children" overflow="auto" gap={ 0 }>
{ roomVisitData &&
<AutoSizer defaultWidth={ 400 } defaultHeight={ 200 }>
{ ({ height, width }) =>
{
return (
<List
width={ width }
height={ height }
rowCount={ roomVisitData.rooms.length }
rowHeight={ 20 }
className={'log-entry-container' }
rowRenderer={ RowRenderer }
/>
);
} }
</AutoSizer> }
</Column>
</Column>
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -0,0 +1,46 @@
import { ModMessageMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useState } from 'react';
import { Button, Text } from '../../../../common';
import { NotificationAlertEvent } from '../../../../events';
import { dispatchUiEvent, SendMessageHook } from '../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { ISelectedUser } from '../../utils/ISelectedUser';
interface ModToolsUserSendMessageViewProps
{
user: ISelectedUser;
onCloseClick: () => void;
}
export const ModToolsUserSendMessageView: FC<ModToolsUserSendMessageViewProps> = props =>
{
const { user = null, onCloseClick = null } = props;
const [ message, setMessage ] = useState('');
const sendMessage = useCallback(() =>
{
if(message.trim().length === 0)
{
dispatchUiEvent(new NotificationAlertEvent([ 'Please write a message to user.' ], null, null, null, 'Error', null));
return;
}
SendMessageHook(new ModMessageMessageComposer(user.userId, message, -999));
onCloseClick();
}, [ message, user, onCloseClick ]);
if(!user) return null;
return (
<NitroCardView className="nitro-mod-tools-user-message" simple>
<NitroCardHeaderView headerText={'Send Message'} onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black">
<Text>Message To: { user.username }</Text>
<textarea className="form-control" value={ message } onChange={ event => setMessage(event.target.value) }></textarea>
<Button fullWidth onClick={ sendMessage }>Send message</Button>
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -1,13 +1,19 @@
import { FriendlyTime, GetModeratorUserInfoMessageComposer, ModeratorUserInfoData, ModeratorUserInfoEvent } from '@nitrots/nitro-renderer'; import { FriendlyTime, GetModeratorUserInfoMessageComposer, ModeratorUserInfoData, ModeratorUserInfoEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { LocalizeText } from '../../../../../api'; import { LocalizeText } from '../../../../api';
import { ModToolsOpenUserChatlogEvent } from '../../../../../events/mod-tools/ModToolsOpenUserChatlogEvent'; import { Button, Column, Grid } from '../../../../common';
import { CreateMessageHook, dispatchUiEvent, SendMessageHook } from '../../../../../hooks'; import { ModToolsOpenUserChatlogEvent } from '../../../../events/mod-tools/ModToolsOpenUserChatlogEvent';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView, NitroLayoutButton, NitroLayoutGrid, NitroLayoutGridColumn } from '../../../../../layout'; import { CreateMessageHook, dispatchUiEvent, SendMessageHook } from '../../../../hooks';
import { ModToolsUserModActionView } from '../user-mod-action/ModToolsUserModActionView'; import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { ModToolsUserRoomVisitsView } from '../user-room-visits/ModToolsUserRoomVisitsView'; import { ModToolsUserModActionView } from './ModToolsUserModActionView';
import { ModToolsSendUserMessageView } from '../user-sendmessage/ModToolsSendUserMessageView'; import { ModToolsUserRoomVisitsView } from './ModToolsUserRoomVisitsView';
import { ModToolsUserViewProps } from './ModToolsUserView.types'; import { ModToolsUserSendMessageView } from './ModToolsUserSendMessageView';
interface ModToolsUserViewProps
{
userId: number;
onCloseClick: () => void;
}
export const ModToolsUserView: FC<ModToolsUserViewProps> = props => export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
{ {
@ -17,11 +23,6 @@ export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
const [ modActionVisible, setModActionVisible ] = useState(false); const [ modActionVisible, setModActionVisible ] = useState(false);
const [ roomVisitsVisible, setRoomVisitsVisible ] = useState(false); const [ roomVisitsVisible, setRoomVisitsVisible ] = useState(false);
useEffect(() =>
{
SendMessageHook(new GetModeratorUserInfoMessageComposer(userId));
}, [ userId ]);
const onModtoolUserInfoEvent = useCallback((event: ModeratorUserInfoEvent) => const onModtoolUserInfoEvent = useCallback((event: ModeratorUserInfoEvent) =>
{ {
const parser = event.getParser(); const parser = event.getParser();
@ -29,7 +30,7 @@ export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
if(!parser || parser.data.userId !== userId) return; if(!parser || parser.data.userId !== userId) return;
setUserInfo(parser.data); setUserInfo(parser.data);
}, [setUserInfo, userId]); }, [ userId ]);
CreateMessageHook(ModeratorUserInfoEvent, onModtoolUserInfoEvent); CreateMessageHook(ModeratorUserInfoEvent, onModtoolUserInfoEvent);
@ -98,52 +99,58 @@ export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
]; ];
}, [ userInfo ]); }, [ userInfo ]);
useEffect(() =>
{
SendMessageHook(new GetModeratorUserInfoMessageComposer(userId));
}, [ userId ]);
if(!userInfo) return null; if(!userInfo) return null;
return ( return (
<> <>
<NitroCardView className="nitro-mod-tools-user" simple={true}> <NitroCardView className="nitro-mod-tools-user" simple>
<NitroCardHeaderView headerText={ LocalizeText('modtools.userinfo.title', [ 'username' ], [ userInfo.userName ]) } onCloseClick={ () => onCloseClick() } /> <NitroCardHeaderView headerText={ LocalizeText('modtools.userinfo.title', [ 'username' ], [ userInfo.userName ]) } onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black"> <NitroCardContentView className="text-black">
<NitroLayoutGrid> <Grid overflow="hidden">
<NitroLayoutGridColumn size={ 8 }> <Column size={ 8 } overflow="auto">
<table className="table table-striped table-sm table-text-small text-black m-0"> <table className="table table-striped table-sm table-text-small text-black m-0">
<tbody> <tbody>
{ userProperties.map( (property, index) => { userProperties.map( (property, index) =>
{ {
return ( return (
<tr key={index}> <tr key={ index }>
<th scope="row">{ LocalizeText(property.localeKey) }</th> <th scope="row">{ LocalizeText(property.localeKey) }</th>
<td> <td>
{ property.value } { property.value }
{ property.showOnline && <i className={ `icon icon-pf-${ userInfo.online ? 'online' : 'offline' } ms-2` } /> } { property.showOnline &&
<i className={ `icon icon-pf-${ userInfo.online ? 'online' : 'offline' } ms-2` } /> }
</td> </td>
</tr> </tr>
); );
}) } }) }
</tbody> </tbody>
</table> </table>
</NitroLayoutGridColumn> </Column>
<NitroLayoutGridColumn size={ 4 }> <Column size={ 4 } gap={ 1 }>
<NitroLayoutButton variant="primary" size="sm" onClick={ event => dispatchUiEvent(new ModToolsOpenUserChatlogEvent(userId)) }> <Button onClick={ event => dispatchUiEvent(new ModToolsOpenUserChatlogEvent(userId)) }>
Room Chat Room Chat
</NitroLayoutButton> </Button>
<NitroLayoutButton variant="primary" size="sm" onClick={ event => setSendMessageVisible(!sendMessageVisible) }> <Button onClick={ event => setSendMessageVisible(!sendMessageVisible) }>
Send Message Send Message
</NitroLayoutButton> </Button>
<NitroLayoutButton variant="primary" size="sm" onClick={ event => setRoomVisitsVisible(!roomVisitsVisible) }> <Button onClick={ event => setRoomVisitsVisible(!roomVisitsVisible) }>
Room Visits Room Visits
</NitroLayoutButton> </Button>
<NitroLayoutButton variant="primary" size="sm" onClick={ event => setModActionVisible(!modActionVisible) }> <Button onClick={ event => setModActionVisible(!modActionVisible) }>
Mod Action Mod Action
</NitroLayoutButton> </Button>
</NitroLayoutGridColumn> </Column>
</NitroLayoutGrid> </Grid>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
{ sendMessageVisible && { sendMessageVisible &&
<ModToolsSendUserMessageView user={ { userId: userId, username: userInfo.userName } } onCloseClick={ () => setSendMessageVisible(false) } /> } <ModToolsUserSendMessageView user={ { userId: userId, username: userInfo.userName } } onCloseClick={ () => setSendMessageVisible(false) } /> }
{ modActionVisible && { modActionVisible &&
<ModToolsUserModActionView user={ { userId: userId, username: userInfo.userName } } onCloseClick={ () => setModActionVisible(false) } /> } <ModToolsUserModActionView user={ { userId: userId, username: userInfo.userName } } onCloseClick={ () => setModActionVisible(false) } /> }
{ roomVisitsVisible && { roomVisitsVisible &&

View File

@ -1,11 +1,16 @@
import { createContext, FC, useContext } from 'react'; import { createContext, FC, ProviderProps, useContext } from 'react';
import { IPurseContext, PurseContextProps } from './PurseContext.types'; import { IPurse } from './common/IPurse';
interface IPurseContext
{
purse: IPurse;
}
const PurseContext = createContext<IPurseContext>({ const PurseContext = createContext<IPurseContext>({
purse: null purse: null
}); });
export const PurseContextProvider: FC<PurseContextProps> = props => export const PurseContextProvider: FC<ProviderProps<IPurseContext>> = props =>
{ {
return <PurseContext.Provider value={ props.value }>{ props.children }</PurseContext.Provider> return <PurseContext.Provider value={ props.value }>{ props.children }</PurseContext.Provider>
} }

View File

@ -2,10 +2,9 @@ import { ActivityPointNotificationMessageEvent, UserCreditsEvent, UserCurrencyEv
import { FC, useCallback } from 'react'; import { FC, useCallback } from 'react';
import { CREDITS, DUCKETS, PlaySound } from '../../api/utils/PlaySound'; import { CREDITS, DUCKETS, PlaySound } from '../../api/utils/PlaySound';
import { CreateMessageHook } from '../../hooks/messages/message-event'; import { CreateMessageHook } from '../../hooks/messages/message-event';
import { usePurseContext } from './context/PurseContext'; import { usePurseContext } from './PurseContext';
import { PurseMessageHandlerProps } from './PurseMessageHandler.types';
export const PurseMessageHandler: FC<PurseMessageHandlerProps> = props => export const PurseMessageHandler: FC<{}> = props =>
{ {
const { purse = null } = usePurseContext(); const { purse = null } = usePurseContext();

View File

@ -0,0 +1,26 @@
.nitro-purse-container {
font-size: $font-size-sm;
pointer-events: all;
.nitro-purse {
background-color: rgba($dark, 0.95);
box-shadow: inset 0px 5px lighten(rgba($dark, 0.6), 2.5), inset 0 -4px darken(rgba($dark, 0.6), 4);
.nitro-purse-subscription {
background-color: rgba($light, 0.1);
}
.nitro-purse-button {
padding: 3px 2px;
&:hover {
background-color: rgba($light, 0.1);
}
}
}
.nitro-purse-seasonal-currency {
background-color: rgba($dark, .95);
box-shadow: inset 0px 5px lighten(rgba($dark, .6),2.5), inset 0 -4px darken(rgba($dark, .6), 4);
}
}

View File

@ -0,0 +1,137 @@
import { FriendlyTime, HabboClubLevelEnum, UserCurrencyComposer, UserSubscriptionComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { CreateLinkEvent, GetConfiguration, LocalizeText } from '../../api';
import { Column, Flex, Grid, Text } from '../../common';
import { HcCenterEvent } from '../../events/hc-center/HcCenterEvent';
import { UserSettingsUIEvent } from '../../events/user-settings/UserSettingsUIEvent';
import { dispatchUiEvent } from '../../hooks';
import { SendMessageHook } from '../../hooks/messages/message-event';
import { CurrencyIcon } from '../../views/shared/currency-icon/CurrencyIcon';
import { IPurse } from './common/IPurse';
import { Purse } from './common/Purse';
import { PurseContextProvider } from './PurseContext';
import { PurseMessageHandler } from './PurseMessageHandler';
import { CurrencyView } from './views/CurrencyView';
import { SeasonalView } from './views/SeasonalView';
export let GLOBAL_PURSE: IPurse = null;
export const PurseView: FC<{}> = props =>
{
const [ purse, setPurse ] = useState<IPurse>(new Purse());
const [ updateId, setUpdateId ] = useState(-1);
const handleUserSettingsClick = () => dispatchUiEvent(new UserSettingsUIEvent(UserSettingsUIEvent.TOGGLE_USER_SETTINGS));
const handleHelpCenterClick = () => CreateLinkEvent('help/show');
const handleHcCenterClick = () => dispatchUiEvent(new HcCenterEvent(HcCenterEvent.TOGGLE_HC_CENTER));
const displayedCurrencies = useMemo(() => GetConfiguration<number[]>('system.currency.types', []), []);
const currencyDisplayNumberShort = useMemo(() => GetConfiguration<boolean>('currency.display.number.short', false), []);
const getClubText = useMemo(() =>
{
const totalDays = ((purse.clubPeriods * 31) + purse.clubDays);
const minutesUntilExpiration = purse.minutesUntilExpiration;
if(purse.clubLevel === HabboClubLevelEnum.NO_CLUB) return LocalizeText('purse.clubdays.zero.amount.text');
else if((minutesUntilExpiration > -1) && (minutesUntilExpiration < (60 * 24))) return FriendlyTime.shortFormat(minutesUntilExpiration * 60);
else return FriendlyTime.shortFormat(totalDays * 86400);
}, [ purse ]);
const getCurrencyElements = useCallback((offset: number, limit: number = -1, seasonal: boolean = false) =>
{
if(!purse.activityPoints.size) return null;
const types = Array.from(purse.activityPoints.keys()).filter(type => (displayedCurrencies.indexOf(type) >= 0));
let count = 0;
while(count < offset)
{
types.shift();
count++;
}
count = 0;
const elements: JSX.Element[] = [];
for(const type of types)
{
if((limit > -1) && (count === limit)) break;
if(seasonal) elements.push(<SeasonalView key={ type } type={ type } amount={ purse.activityPoints.get(type) } />);
else elements.push(<CurrencyView key={ type } type={ type } amount={ purse.activityPoints.get(type) } short={ currencyDisplayNumberShort } />);
count++;
}
return elements;
}, [ purse, displayedCurrencies, currencyDisplayNumberShort ]);
useEffect(() =>
{
const purse = new Purse();
GLOBAL_PURSE = purse;
purse.notifier = () => setUpdateId(prevValue => (prevValue + 1));
setPurse(purse);
return () => (purse.notifier = null);
}, []);
useEffect(() =>
{
if(!purse) return;
SendMessageHook(new UserCurrencyComposer());
}, [ purse ]);
useEffect(() =>
{
SendMessageHook(new UserSubscriptionComposer('habbo_club'));
const interval = setInterval(() => SendMessageHook(new UserSubscriptionComposer('habbo_club')), 50000);
return () => clearInterval(interval);
}, [ purse ]);
if(!purse) return null;
return (
<PurseContextProvider value={ { purse } }>
<PurseMessageHandler />
<Column className="nitro-purse-container" gap={ 1 }>
<Flex className="nitro-purse rounded-bottom p-1">
<Grid fullWidth gap={ 1 }>
<Column justifyContent="center" size={ 6 } gap={ 0 }>
<CurrencyView type={ -1 } amount={ purse.credits } short={ currencyDisplayNumberShort } />
{ getCurrencyElements(0, 2) }
</Column>
<Column center pointer size={ 4 } gap={ 1 } className="nitro-purse-subscription rounded" onClick={ handleHcCenterClick }>
<CurrencyIcon type="hc" />
<Text variant="white">{ getClubText }</Text>
</Column>
<Column justifyContent="center" size={ 2 } gap={ 0 }>
<Flex center pointer fullHeight className="nitro-purse-button p-1 rounded" onClick={ handleHelpCenterClick }>
<i className="icon icon-help"/>
</Flex>
<Flex center pointer fullHeight className="nitro-purse-button p-1 rounded" onClick={ handleUserSettingsClick } >
<i className="icon icon-cog"/>
</Flex>
</Column>
</Grid>
</Flex>
{ getCurrencyElements(2, -1, true) }
</Column>
</PurseContextProvider>
);
}

View File

@ -0,0 +1,40 @@
import { FC, useMemo } from 'react';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
import { LocalizeFormattedNumber, LocalizeShortNumber } from '../../../api';
import { Flex, Text } from '../../../common';
import { CurrencyIcon } from '../../../views/shared/currency-icon/CurrencyIcon';
interface CurrencyViewProps
{
type: number;
amount: number;
short: boolean;
}
export const CurrencyView: FC<CurrencyViewProps> = props =>
{
const { type = -1, amount = -1, short = false } = props;
const element = useMemo(() =>
{
return (
<Flex justifyContent="end" pointer gap={ 1 } className="nitro-purse-button rounded">
<Text truncate textEnd variant="white" grow>{ short ? LocalizeShortNumber(amount) : LocalizeFormattedNumber(amount) }</Text>
<CurrencyIcon type={ type } />
</Flex>);
}, [ amount, short, type ]);
if(!short) return element;
return (
<OverlayTrigger
placement="left"
overlay={
<Tooltip id={ `tooltip-${ type }` }>
{ LocalizeFormattedNumber(amount) }
</Tooltip>
}>
{ element }
</OverlayTrigger>
);
}

View File

@ -0,0 +1,25 @@
import { FC } from 'react';
import { LocalizeFormattedNumber, LocalizeText } from '../../../api';
import { Flex, Text } from '../../../common';
import { CurrencyIcon } from '../../../views/shared/currency-icon/CurrencyIcon';
interface SeasonalViewProps
{
type: number;
amount: number;
}
export const SeasonalView: FC<SeasonalViewProps> = props =>
{
const { type = -1, amount = -1 } = props;
return (
<Flex justifyContent="between" className="nitro-purse-seasonal-currency p-2 rounded">
<Text variant="white">{ LocalizeText(`purse.seasonal.currency.${ type }`) }</Text>
<Flex gap={ 1 }>
<Text variant="white">{ LocalizeFormattedNumber(amount) }</Text>
<CurrencyIcon type={ type } />
</Flex>
</Flex>
);
}

View File

@ -0,0 +1,18 @@
import { FC } from 'react';
import { Column } from '../../common';
import { NotificationCenterView } from '../../views/notification-center/NotificationCenterView';
import { GroupRoomInformationView } from '../groups/views/room-information/GroupRoomInformationView';
import { PurseView } from '../purse/PurseView';
export const RightSideView: FC<{}> = props =>
{
return (
<div className="nitro-right-side">
<Column position="relative" gap={ 1 }>
<PurseView />
<GroupRoomInformationView />
<NotificationCenterView />
</Column>
</div>
);
}

View File

@ -1,10 +1,11 @@
import { IRoomSession, RoomEngineEvent, RoomId, RoomSessionEvent } from '@nitrots/nitro-renderer'; import { IRoomSession, RoomEngineEvent, RoomId, RoomSessionEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useState } from 'react'; import { FC, useCallback, useState } from 'react';
import { GetRoomSession, SetActiveRoomId, StartRoomSession } from '../../api'; import { GetRoomSession, SetActiveRoomId, StartRoomSession } from '../../api';
import { Base } from '../../common';
import { useRoomEngineEvent } from '../../hooks/events/nitro/room/room-engine-event'; import { useRoomEngineEvent } from '../../hooks/events/nitro/room/room-engine-event';
import { useRoomSessionManagerEvent } from '../../hooks/events/nitro/session/room-session-manager-event'; import { useRoomSessionManagerEvent } from '../../hooks/events/nitro/session/room-session-manager-event';
import { TransitionAnimation, TransitionAnimationTypes } from '../../layout'; import { TransitionAnimation, TransitionAnimationTypes } from '../../layout';
import { RoomView } from '../room/RoomView'; import { RoomView } from '../../views/room/RoomView';
export const RoomHostView: FC<{}> = props => export const RoomHostView: FC<{}> = props =>
{ {
@ -51,9 +52,9 @@ export const RoomHostView: FC<{}> = props =>
return ( return (
<TransitionAnimation type={ TransitionAnimationTypes.FADE_IN } inProp={ !!roomSession } timeout={ 300 }> <TransitionAnimation type={ TransitionAnimationTypes.FADE_IN } inProp={ !!roomSession } timeout={ 300 }>
<div className="nitro-room-host w-100 h-100"> <Base fit>
<RoomView roomSession={ roomSession } /> <RoomView roomSession={ roomSession } />
</div> </Base>
</TransitionAnimation> </TransitionAnimation>
); );
} }

View File

@ -1,6 +1,7 @@
import { RoomObjectCategory } from '@nitrots/nitro-renderer'; import { RoomObjectCategory } from '@nitrots/nitro-renderer';
import { FC, useEffect } from 'react'; import { FC, useEffect } from 'react';
import { GetRoomEngine, GetRoomSession } from '../../api'; import { GetRoomEngine, GetRoomSession } from '../../api';
import { Base, Flex } from '../../common';
import { ItemCountView } from '../../views/shared/item-count/ItemCountView'; import { ItemCountView } from '../../views/shared/item-count/ItemCountView';
import { ToolbarViewItems } from './common/ToolbarViewItems'; import { ToolbarViewItems } from './common/ToolbarViewItems';
@ -24,35 +25,15 @@ export const ToolbarMeView: FC<ToolbarMeViewProps> = props =>
}, []); }, []);
return ( return (
<div className="d-flex nitro-toolbar-me px-1 py-2"> <Flex alignItems="center" className="nitro-toolbar-me p-2" gap={ 2 }>
<div className="navigation-items"> <Base pointer className="navigation-item icon icon-me-achievements" onClick={ () => handleToolbarItemClick(ToolbarViewItems.ACHIEVEMENTS_ITEM) }>
<div className="navigation-item">
<i className="icon icon-me-talents"></i>
</div>
<div className="navigation-item">
<i className="icon icon-me-helper-tool"></i>
</div>
<div className="navigation-item" onClick={ () => handleToolbarItemClick(ToolbarViewItems.ACHIEVEMENTS_ITEM) }>
<i className="icon icon-me-achievements"></i>
{ (unseenAchievementCount > 0) && { (unseenAchievementCount > 0) &&
<ItemCountView count={ unseenAchievementCount } /> } <ItemCountView count={ unseenAchievementCount } /> }
</div> </Base>
<div className="navigation-item" onClick={ () => handleToolbarItemClick(ToolbarViewItems.PROFILE_ITEM) }> <Base pointer className="navigation-item icon icon-me-profile" onClick={ () => handleToolbarItemClick(ToolbarViewItems.PROFILE_ITEM) } />
<i className="icon icon-me-profile"></i> <Base pointer className="navigation-item icon icon-me-rooms" />
</div> <Base pointer className="navigation-item icon icon-me-clothing" onClick={ () => handleToolbarItemClick(ToolbarViewItems.CLOTHING_ITEM) } />
<div className="navigation-item"> <Base pointer className="navigation-item icon icon-me-settings" onClick={ () => handleToolbarItemClick(ToolbarViewItems.SETTINGS_ITEM) } />
<i className="icon icon-me-rooms"></i> </Flex>
</div>
<div className="navigation-item" onClick={ () => handleToolbarItemClick(ToolbarViewItems.CLOTHING_ITEM) }>
<i className="icon icon-me-clothing"></i>
</div>
<div className="navigation-item">
<i className="icon icon-me-forums"></i>
</div>
<div className="navigation-item" onClick={ () => handleToolbarItemClick(ToolbarViewItems.SETTINGS_ITEM) }>
<i className="icon icon-me-settings"></i>
</div>
</div>
</div>
); );
} }

View File

@ -1,42 +1,16 @@
.nitro-toolbar-container { .nitro-toolbar {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: $toolbar-height; height: $toolbar-height;
z-index: $toolbar-zindex; z-index: $toolbar-zindex;
.nitro-toolbar {
height: 100%;
pointer-events: all; pointer-events: all;
background: rgba($dark, 0.95); background: rgba($dark, 0.95);
box-shadow: inset 0px 5px lighten(rgba($dark, 0.6), 2.5), box-shadow: inset 0px 5px lighten(rgba($dark, 0.6), 2.5),
inset 0 -4px darken(rgba($dark, 0.6), 4); inset 0 -4px darken(rgba($dark, 0.6), 4);
#toolbar-chat-input-container {
margin: 0 10px;
@include media-breakpoint-down(sm) {
width: 0px;
height: 0px;
}
}
.navigation-items {
display: flex;
align-items: center;
&.navigation-avatar {
border-right: 1px solid rgba(0, 0, 0, 0.3);
}
.navigation-item { .navigation-item {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
//margin: 0 1px;
position: relative;
&.item-avatar { &.item-avatar {
width: 50px; width: 50px;
@ -49,63 +23,45 @@
} }
} }
.icon, &:hover {
&.item-avatar {
position: relative;
//transition: transform .2s ease-out;
&:hover,
&.active {
-webkit-transform: translate(-1px, -1px); -webkit-transform: translate(-1px, -1px);
transform: translate(-1px, -1px); transform: translate(-1px, -1px);
filter: drop-shadow(2px 2px 0 rgba($black, 0.8)); filter: drop-shadow(2px 2px 0 rgba($black, 0.8));
} }
}
.avatar-image { &.active,
pointer-events: none; &:active {
} -webkit-transform: translate(0px, 0px);
transform: translate(0px, 0px);
.chat-input-container { filter: drop-shadow(2px 2px 0 rgba($black, 0.8));
left: 60px;
}
} }
} }
.nitro-toolbar-me-menu { #toolbar-chat-input-container {
bottom: 77px;
left: 200px; @include media-breakpoint-down(sm) {
width: 0px;
height: 0px;
}
}
}
.nitro-toolbar-me {
position: absolute; position: absolute;
font-size: 12px; bottom: 60px;
left: 15px;
z-index: $toolbar-memenu-zindex; z-index: $toolbar-memenu-zindex;
background: rgba(20, 20, 20, .95);
border: 1px solid #101010;
box-shadow: inset 2px 2px rgba(255, 255, 255, .1), inset -2px -2px rgba(255, 255, 255, .1);
border-radius: $border-radius;
.list-group { .navigation-item {
.list-group-item { transition: filter .2s ease-out;
min-width: 70px;
transition: all 0.3s;
font-size: 10px;
text-align: center;
i {
filter: grayscale(1); filter: grayscale(1);
}
&:hover { &:hover {
color: $cyan; filter: grayscale(0) drop-shadow(2px 2px 0 rgba($black, 0.8));
text-decoration: underline;
i {
filter: grayscale(0);
}
}
.count {
top: 0px;
right: 5px;
font-size: 10px;
}
}
}
} }
} }
} }
@ -122,45 +78,3 @@
drop-shadow(-2px 1px 0 rgba($white, 1)) drop-shadow(-2px 1px 0 rgba($white, 1))
drop-shadow(0 -2px 0 rgba($white, 1)); drop-shadow(0 -2px 0 rgba($white, 1));
} }
.nitro-toolbar-me {
position: absolute;
bottom: 65px;
left: 15px;
z-index: $toolbar-me-zindex;
background: rgba(20, 20, 20, .95);
border: 1px solid #101010;
box-shadow: inset 2px 2px rgba(255, 255, 255, .1), inset -2px -2px rgba(255, 255, 255, .1);
border-radius: $border-radius;
.navigation-items {
display: flex;
align-items: center;
&.navigation-avatar {
border-right: 1px solid rgba(0, 0, 0, .3);
}
.navigation-item {
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
cursor: pointer;
width: 50px;
font-size: 11px;
.icon {
transition: filter .2s ease-out;
filter: grayscale(1);
}
&:hover {
.icon {
filter: grayscale(0) drop-shadow(2px 2px 0 rgba($black, 0.8));
}
}
}
}
}

View File

@ -1,6 +1,7 @@
import { Dispose, DropBounce, EaseOut, FigureUpdateEvent, JumpBy, Motions, NitroToolbarAnimateIconEvent, Queue, UserInfoDataParser, UserInfoEvent, Wait } from '@nitrots/nitro-renderer'; import { Dispose, DropBounce, EaseOut, FigureUpdateEvent, JumpBy, Motions, NitroToolbarAnimateIconEvent, Queue, UserInfoDataParser, UserInfoEvent, Wait } from '@nitrots/nitro-renderer';
import { FC, useCallback, useState } from 'react'; import { FC, useCallback, useState } from 'react';
import { CreateLinkEvent, GetRoomSession, GetRoomSessionManager, GetSessionDataManager, GetUserProfile, GoToDesktop, OpenMessengerChat } from '../../api'; import { CreateLinkEvent, GetRoomSession, GetRoomSessionManager, GetSessionDataManager, GetUserProfile, GoToDesktop, OpenMessengerChat } from '../../api';
import { Base, Flex } from '../../common';
import { AvatarEditorEvent, FriendsEvent, FriendsMessengerIconEvent, FriendsRequestCountEvent, InventoryEvent, NavigatorEvent, RoomWidgetCameraEvent } from '../../events'; import { AvatarEditorEvent, FriendsEvent, FriendsMessengerIconEvent, FriendsRequestCountEvent, InventoryEvent, NavigatorEvent, RoomWidgetCameraEvent } from '../../events';
import { AchievementsUIEvent, AchievementsUIUnseenCountEvent } from '../../events/achievements'; import { AchievementsUIEvent, AchievementsUIUnseenCountEvent } from '../../events/achievements';
import { UnseenItemTrackerUpdateEvent } from '../../events/inventory/UnseenItemTrackerUpdateEvent'; import { UnseenItemTrackerUpdateEvent } from '../../events/inventory/UnseenItemTrackerUpdateEvent';
@ -181,64 +182,47 @@ export const ToolbarView: FC<ToolbarViewProps> = props =>
}, []); }, []);
return ( return (
<div className="nitro-toolbar-container"> <>
<TransitionAnimation type={ TransitionAnimationTypes.FADE_IN } inProp={ isMeExpanded } timeout={ 300 }> <TransitionAnimation type={ TransitionAnimationTypes.FADE_IN } inProp={ isMeExpanded } timeout={ 300 }>
<ToolbarMeView unseenAchievementCount={ unseenAchievementCount } handleToolbarItemClick={ handleToolbarItemClick } /> <ToolbarMeView unseenAchievementCount={ unseenAchievementCount } handleToolbarItemClick={ handleToolbarItemClick } />
</TransitionAnimation> </TransitionAnimation>
<div className="d-flex justify-content-between align-items-center nitro-toolbar py-1 px-3"> <Flex alignItems="center" justifyContent="between" gap={ 2 } className="nitro-toolbar py-1 px-3">
<div className="d-flex align-items-center"> <Flex gap={ 2 } alignItems="center">
<div className="navigation-items gap-2"> <Flex alignItems="center" gap={ 2 }>
<div className={ 'navigation-item item-avatar ' + (isMeExpanded ? 'active ' : '') } onClick={ event => setMeExpanded(!isMeExpanded) }> <Flex center pointer className={ 'navigation-item item-avatar ' + (isMeExpanded ? 'active ' : '') } onClick={ event => setMeExpanded(!isMeExpanded) }>
<AvatarImageView figure={ userFigure } direction={ 2 } /> <AvatarImageView figure={ userFigure } direction={ 2 } />
{ (unseenAchievementCount > 0) && { (unseenAchievementCount > 0) &&
<ItemCountView count={ unseenAchievementCount } /> } <ItemCountView count={ unseenAchievementCount } /> }
</div> </Flex>
{ isInRoom && ( { isInRoom &&
<div className="navigation-item" onClick={ visitDesktop }> <Base pointer className="navigation-item icon icon-habbo" onClick={ visitDesktop } /> }
<i className="icon icon-habbo"></i> { !isInRoom &&
</div>) } <Base pointer className="navigation-item icon icon-house" onClick={ event => CreateLinkEvent('navigator/goto/home') } /> }
{ !isInRoom && ( <Base pointer className="navigation-item icon icon-rooms" onClick={ event => handleToolbarItemClick(ToolbarViewItems.NAVIGATOR_ITEM) } />
<div className="navigation-item" onClick={ event => CreateLinkEvent('navigator/goto/home') }> <Base pointer className="navigation-item icon icon-catalog" onClick={ event => handleToolbarItemClick(ToolbarViewItems.CATALOG_ITEM) } />
<i className="icon icon-house"></i> <Base pointer className="navigation-item icon icon-inventory" onClick={ event => handleToolbarItemClick(ToolbarViewItems.INVENTORY_ITEM) }>
</div>) }
<div className="navigation-item" onClick={ event => handleToolbarItemClick(ToolbarViewItems.NAVIGATOR_ITEM) }>
<i className="icon icon-rooms"></i>
</div>
<div className="navigation-item" onClick={ event => handleToolbarItemClick(ToolbarViewItems.CATALOG_ITEM) }>
<i className="icon icon-catalog"></i>
</div>
<div className="navigation-item" onClick={ event => handleToolbarItemClick(ToolbarViewItems.INVENTORY_ITEM) }>
<i className="icon icon-inventory"></i>
{ (unseenInventoryCount > 0) && { (unseenInventoryCount > 0) &&
<ItemCountView count={ unseenInventoryCount } /> } <ItemCountView count={ unseenInventoryCount } /> }
</div> </Base>
{ isInRoom && ( { isInRoom &&
<div className="navigation-item" onClick={ event => handleToolbarItemClick(ToolbarViewItems.CAMERA_ITEM) }> <Base pointer className="navigation-item icon icon-camera" onClick={ event => handleToolbarItemClick(ToolbarViewItems.CAMERA_ITEM) } /> }
<i className="icon icon-camera"></i> { isMod &&
</div>) } <Base pointer className="navigation-item icon icon-modtools" onClick={ event => handleToolbarItemClick(ToolbarViewItems.MOD_TOOLS_ITEM) } /> }
{ isMod && ( </Flex>
<div className="navigation-item" onClick={ event => handleToolbarItemClick(ToolbarViewItems.MOD_TOOLS_ITEM) }> <Flex alignItems="center" id="toolbar-chat-input-container" />
<i className="icon icon-modtools"></i> </Flex>
</div>) } <Flex alignItems="center" gap={ 2 }>
</div> <Flex gap={ 2 }>
<div id="toolbar-chat-input-container" className="d-flex align-items-center" /> <Base pointer className="navigation-item icon icon-friendall" onClick={ event => handleToolbarItemClick(ToolbarViewItems.FRIEND_LIST_ITEM) }>
</div>
<div className="d-flex align-items-center gap-2">
<div className="navigation-items gap-2">
<div className="navigation-item" onClick={ event => handleToolbarItemClick(ToolbarViewItems.FRIEND_LIST_ITEM) }>
<i className="icon icon-friendall"></i>
{ (unseenFriendRequestCount > 0) && { (unseenFriendRequestCount > 0) &&
<ItemCountView count={ unseenFriendRequestCount } /> } <ItemCountView count={ unseenFriendRequestCount } /> }
</div> </Base>
{ ((chatIconType === CHAT_ICON_SHOWING) || (chatIconType === CHAT_ICON_UNREAD)) && { ((chatIconType === CHAT_ICON_SHOWING) || (chatIconType === CHAT_ICON_UNREAD)) &&
<div className="navigation-item" onClick={ event => handleToolbarItemClick(ToolbarViewItems.FRIEND_CHAT_ITEM) }> <Base pointer className={ `navigation-item icon icon-message ${ (chatIconType === CHAT_ICON_UNREAD) && 'is-unread' }` } onClick={ event => handleToolbarItemClick(ToolbarViewItems.FRIEND_CHAT_ITEM) } /> }
{ (chatIconType === CHAT_ICON_SHOWING) && <i className="icon icon-message" /> } </Flex>
{ (chatIconType === CHAT_ICON_UNREAD) && <i className="icon icon-message is-unseen" /> } <Base id="toolbar-friend-bar-container" className="d-none d-lg-block" />
</div> } </Flex>
</div> </Flex>
<div id="toolbar-friend-bar-container" className="d-none d-lg-block" /> </>
</div>
</div>
</div>
); );
} }

View File

@ -1,8 +1,3 @@
export interface ToolbarViewProps
{
isInRoom: boolean;
}
export class ToolbarViewItems export class ToolbarViewItems
{ {
public static NAVIGATOR_ITEM: string = 'TVI_NAVIGATOR_ITEM'; public static NAVIGATOR_ITEM: string = 'TVI_NAVIGATOR_ITEM';

View File

@ -0,0 +1,106 @@
.user-profile {
width: 560px;
.content-area {
color: black;
}
.user-container {
border-right: 1px solid gray;
.avatar-image {
left: -10px;
}
.add-friend {
margin: 5px;
margin-left: 10px;
}
}
.badge-container {
min-height: 50px;
background: rgba(0, 0, 0, 0.1);
border-radius: 5px;
margin: 0px;
margin-bottom: 2px;
}
.rooms-button-container {
border-top: 1px solid gray;
border-bottom: 1px solid gray;
padding: 1px;
.rooms-button {
display: inline-block;
text-align: center;
height: 100%;
text-decoration: underline;
margin-left: 10px;
}
}
.friends-container {
height: 100%;
}
}
.profile-groups {
height: 219px;
.profile-groups-item {
width: 50px;
height: 50px;
border-radius: $border-radius;
border-color: $grid-border-color !important;
background-color: $grid-bg-color;
border: nth(map-values($border-widths), 2) solid;
&.active {
border-color: $grid-active-border-color !important;
background-color: $grid-active-bg-color !important;
}
.icon {
z-index: 1;
top: 0px;
right: 0px;
}
}
}
.relationships-container {
.relationship-container {
.relationship
{
position: relative;
&.advanced {
background-color: white;
padding: 5px;
border-radius: 5px;
}
.relationship-text {
text-decoration: underline;
}
.avatar-image {
position: absolute;
width: 50px;
height: 80px;
right: 0;
margin-top: -60px;
}
}
.others-text {
margin-left: 20px;
height: 21px;
color: #939392;
}
}
}

View File

@ -0,0 +1,104 @@
import { RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, UserCurrentBadgesComposer, UserCurrentBadgesEvent, UserProfileEvent, UserProfileParser, UserRelationshipsComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useState } from 'react';
import { GetSessionDataManager, GetUserProfile, LocalizeText } from '../../api';
import { Column, Flex, Grid } from '../../common';
import { BatchUpdates, CreateMessageHook, SendMessageHook } from '../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../layout';
import { BadgesContainerView } from './views/BadgesContainerView';
import { FriendsContainerView } from './views/FriendsContainerView';
import { GroupsContainerView } from './views/GroupsContainerView';
import { UserContainerView } from './views/UserContainerView';
export const UserProfileView: FC<{}> = props =>
{
const [ userProfile, setUserProfile ] = useState<UserProfileParser>(null);
const [ userBadges, setUserBadges ] = useState<string[]>([]);
const [ userRelationships, setUserRelationships ] = useState<RelationshipStatusInfoMessageParser>(null);
const onClose = () =>
{
BatchUpdates(() =>
{
setUserProfile(null);
setUserBadges([]);
setUserRelationships(null);
});
}
const onLeaveGroup = useCallback(() =>
{
if(userProfile && userProfile.id === GetSessionDataManager().userId)
{
GetUserProfile(userProfile.id);
}
}, [ userProfile ]);
const onUserCurrentBadgesEvent = useCallback((event: UserCurrentBadgesEvent) =>
{
const parser = event.getParser();
if(!userProfile || (parser.userId !== userProfile.id)) return;
setUserBadges(parser.badges);
}, [ userProfile ]);
CreateMessageHook(UserCurrentBadgesEvent, onUserCurrentBadgesEvent);
const OnUserRelationshipsEvent = useCallback((event: RelationshipStatusInfoEvent) =>
{
const parser = event.getParser();
if(!userProfile || (parser.userId !== userProfile.id)) return;
setUserRelationships(parser);
}, [ userProfile ]);
CreateMessageHook(RelationshipStatusInfoEvent, OnUserRelationshipsEvent);
const onUserProfileEvent = useCallback((event: UserProfileEvent) =>
{
const parser = event.getParser();
if(userProfile)
{
BatchUpdates(() =>
{
setUserProfile(null);
setUserBadges([]);
setUserRelationships(null);
});
}
setUserProfile(parser);
SendMessageHook(new UserCurrentBadgesComposer(parser.id));
SendMessageHook(new UserRelationshipsComposer(parser.id));
}, [ userProfile ]);
CreateMessageHook(UserProfileEvent, onUserProfileEvent);
if(!userProfile) return null;
return (
<NitroCardView className="user-profile" simple>
<NitroCardHeaderView headerText={ LocalizeText('extendedprofile.caption') } onCloseClick={ onClose } />
<NitroCardContentView>
<Grid>
<Column size={ 7 } className="user-container">
<UserContainerView userProfile={ userProfile } />
<BadgesContainerView badges={ userBadges } />
</Column>
<Column size={ 5 }>
{
userRelationships && <FriendsContainerView relationships={userRelationships} friendsCount={userProfile.friendsCount} />
}
</Column>
</Grid>
<Flex alignItems="center" className="rooms-button-container">
<i className="icon icon-rooms" /><span className="rooms-button">{LocalizeText('extendedprofile.rooms')}</span>
</Flex>
<GroupsContainerView itsMe={ userProfile.id === GetSessionDataManager().userId } groups={ userProfile.groups } onLeaveGroup={ onLeaveGroup } />
</NitroCardContentView>
</NitroCardView>
)
}

View File

@ -1,7 +1,11 @@
import { FC } from 'react'; import { FC } from 'react';
import { NitroCardGridItemView, NitroCardGridView } from '../../../../layout'; import { NitroCardGridItemView, NitroCardGridView } from '../../../layout';
import { BadgeImageView } from '../../../shared/badge-image/BadgeImageView'; import { BadgeImageView } from '../../../views/shared/badge-image/BadgeImageView';
import { BadgesContainerViewProps } from './BadgesContainerView.types';
interface BadgesContainerViewProps
{
badges: string[];
}
export const BadgesContainerView: FC<BadgesContainerViewProps> = props => export const BadgesContainerView: FC<BadgesContainerViewProps> = props =>
{ {

View File

@ -1,7 +1,13 @@
import { RelationshipStatusInfoMessageParser } from '@nitrots/nitro-renderer';
import { FC } from 'react'; import { FC } from 'react';
import { LocalizeText } from '../../../../api'; import { LocalizeText } from '../../../api';
import { RelationshipsContainerView } from '../relationships-container/RelationshipsContainerView'; import { RelationshipsContainerView } from './RelationshipsContainerView';
import { FriendsContainerViewProps } from './FriendsContainerView.types';
interface FriendsContainerViewProps
{
relationships: RelationshipStatusInfoMessageParser;
friendsCount: number;
}
export const FriendsContainerView: FC<FriendsContainerViewProps> = props => export const FriendsContainerView: FC<FriendsContainerViewProps> = props =>
{ {

View File

@ -1,10 +1,16 @@
import { GroupFavoriteComposer, GroupInformationComposer, GroupInformationEvent, GroupInformationParser } from '@nitrots/nitro-renderer'; import { GroupFavoriteComposer, GroupInformationComposer, GroupInformationEvent, GroupInformationParser, HabboGroupEntryData } from '@nitrots/nitro-renderer';
import classNames from 'classnames'; import classNames from 'classnames';
import { FC, useCallback, useEffect, useState } from 'react'; import { FC, useCallback, useEffect, useState } from 'react';
import { GroupInformationView } from '../../../../components/groups/views/information/GroupInformationView'; import { CreateMessageHook, SendMessageHook } from '../../../hooks';
import { CreateMessageHook, SendMessageHook } from '../../../../hooks'; import { BadgeImageView } from '../../../views/shared/badge-image/BadgeImageView';
import { BadgeImageView } from '../../../shared/badge-image/BadgeImageView'; import { GroupInformationView } from '../../groups/views/information/GroupInformationView';
import { GroupsContainerViewProps } from './GroupsContainerView.types';
interface GroupsContainerViewProps
{
itsMe: boolean;
groups: HabboGroupEntryData[];
onLeaveGroup: () => void;
}
export const GroupsContainerView: FC<GroupsContainerViewProps> = props => export const GroupsContainerView: FC<GroupsContainerViewProps> = props =>
{ {

View File

@ -1,8 +1,13 @@
import { RelationshipStatusEnum, RelationshipStatusInfo } from '@nitrots/nitro-renderer'; import { RelationshipStatusEnum, RelationshipStatusInfo, RelationshipStatusInfoMessageParser } from '@nitrots/nitro-renderer';
import { FC, useCallback } from 'react'; import { FC, useCallback } from 'react';
import { GetUserProfile, LocalizeText } from '../../../../api'; import { GetUserProfile, LocalizeText } from '../../../api';
import { AvatarImageView } from '../../../shared/avatar-image/AvatarImageView'; import { AvatarImageView } from '../../../views/shared/avatar-image/AvatarImageView';
import { RelationshipsContainerViewProps } from './RelationshipsContainerView.types';
interface RelationshipsContainerViewProps
{
relationships: RelationshipStatusInfoMessageParser;
simple?: boolean;
}
export const RelationshipsContainerView: FC<RelationshipsContainerViewProps> = props => export const RelationshipsContainerView: FC<RelationshipsContainerViewProps> = props =>
{ {

View File

@ -1,8 +1,12 @@
import { FriendlyTime } from '@nitrots/nitro-renderer'; import { FriendlyTime, UserProfileParser } from '@nitrots/nitro-renderer';
import { FC, useCallback } from 'react'; import { FC, useCallback } from 'react';
import { GetSessionDataManager, LocalizeText } from '../../../../api'; import { GetSessionDataManager, LocalizeText } from '../../../api';
import { AvatarImageView } from '../../../shared/avatar-image/AvatarImageView'; import { AvatarImageView } from '../../../views/shared/avatar-image/AvatarImageView';
import { UserContainerViewProps } from './UserContainerView.types';
interface UserContainerViewProps
{
userProfile: UserProfileParser;
}
export const UserContainerView: FC<UserContainerViewProps> = props => export const UserContainerView: FC<UserContainerViewProps> = props =>
{ {

View File

@ -1,8 +0,0 @@
import { NitroEvent } from '@nitrots/nitro-renderer';
export class ChatHistoryEvent extends NitroEvent
{
public static SHOW_CHAT_HISTORY: string = 'CHE_SHOW_CHAT_HISTORY';
public static HIDE_CHAT_HISTORY: string = 'CHE_HIDE_CHAT_HISTORY';
public static TOGGLE_CHAT_HISTORY: string = 'CHE_TOGGLE_CHAT_HISTORY';
}

View File

@ -9,6 +9,7 @@ body {
user-select: none; user-select: none;
image-rendering: pixelated; image-rendering: pixelated;
image-rendering: -moz-crisp-edges; image-rendering: -moz-crisp-edges;
scrollbar-width: thin;
} }
img { img {

View File

@ -1,12 +1,11 @@
import { FC, useMemo } from 'react'; import { FC, useMemo } from 'react';
import { Column } from '../../../common/Column'; import { Column, ColumnProps } from '../../../common';
import { useNitroCardContext } from '../context'; import { useNitroCardContext } from '../context';
import { NitroCardContentViewProps } from './NitroCardContextView.types';
export const NitroCardContentView: FC<NitroCardContentViewProps> = props => export const NitroCardContentView: FC<ColumnProps> = props =>
{ {
const { theme = 'primary', classNames = [], ...rest } = props; const { classNames = [], ...rest } = props;
const { simple = false } = useNitroCardContext(); const { theme = 'primary', simple = false } = useNitroCardContext();
const getClassNames = useMemo(() => const getClassNames = useMemo(() =>
{ {

View File

@ -1,7 +0,0 @@
import { ColumnProps } from '../../../common/Column';
export interface NitroCardContentViewProps extends ColumnProps
{
theme?: string;
}

View File

@ -1,2 +1 @@
export * from './NitroCardContentView'; export * from './NitroCardContentView';
export * from './NitroCardContextView.types';

View File

@ -5,8 +5,8 @@ import { NitroCardHeaderViewProps } from './NitroCardHeaderView.types';
export const NitroCardHeaderView: FC<NitroCardHeaderViewProps> = props => export const NitroCardHeaderView: FC<NitroCardHeaderViewProps> = props =>
{ {
const { headerText = null, onCloseClick = null, theme = 'primary' } = props; const { headerText = null, onCloseClick = null } = props;
const { simple = false } = useNitroCardContext(); const { theme = 'primary', simple = false } = useNitroCardContext();
const onMouseDown = useCallback((event: MouseEvent<HTMLDivElement>) => const onMouseDown = useCallback((event: MouseEvent<HTMLDivElement>) =>
{ {

View File

@ -1,16 +1,8 @@
@import "./shared/Shared"; @import "./shared/Shared";
@import "./friends/FriendsView"; @import "./friends/FriendsView";
@import "./hotel-view/HotelView"; @import "./hotel-view/HotelView";
@import "./loading/LoadingView";
@import "./main/MainView";
@import "./notification-center/NotificationCenterView"; @import "./notification-center/NotificationCenterView";
@import "./purse/PurseView";
@import "./right-side/RightSideView";
@import "./room/RoomView"; @import "./room/RoomView";
@import "./room-host/RoomHostView";
@import "./mod-tools/ModToolsView";
@import "./user-profile/UserProfileVew";
@import "./chat-history/ChatHistoryView";
@import "./floorplan-editor/FloorplanEditorView"; @import "./floorplan-editor/FloorplanEditorView";
@import "./nitropedia/NitropediaView"; @import "./nitropedia/NitropediaView";
@import "./hc-center/HcCenterView.scss"; @import "./hc-center/HcCenterView.scss";

View File

@ -1,31 +0,0 @@
.nitro-chat-history {
width: $chat-history-width;
height: $chat-history-height;
background-color: #1C323F;
border: 2px solid rgba(255, 255, 255, 0.5);
border-radius: 0.25rem;
.nitro-card-header-container {
background-color: #3d5f6e;
color: #fff;
}
.chat-history-content {
.chat-history-container {
min-height: 200px;
.chat-history-list {
.chathistory-entry {
.light {
background-color: #121f27;
}
.dark {
background-color: #0d171d;
}
}
}
}
}
}

View File

@ -1,167 +0,0 @@
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AutoSizer, CellMeasurer, CellMeasurerCache, List, ListRowProps, ListRowRenderer, Size } from 'react-virtualized';
import { RenderedRows } from 'react-virtualized/dist/es/List';
import { ChatHistoryEvent } from '../../events/chat-history/ChatHistoryEvent';
import { useUiEvent } from '../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../layout';
import { ChatHistoryMessageHandler } from './ChatHistoryMessageHandler';
import { ChatHistoryState } from './common/ChatHistoryState';
import { SetChatHistory } from './common/GetChatHistory';
import { RoomHistoryState } from './common/RoomHistoryState';
import { ChatHistoryContextProvider } from './context/ChatHistoryContext';
import { ChatEntryType } from './context/ChatHistoryContext.types';
export const ChatHistoryView: FC<{}> = props =>
{
const [ isVisible, setIsVisible ] = useState(false);
const [ needsScroll, setNeedsScroll ] = useState(false);
const [ chatHistoryUpdateId, setChatHistoryUpdateId ] = useState(-1);
const [ roomHistoryUpdateId, setRoomHistoryUpdateId ] = useState(-1);
const [ chatHistoryState, setChatHistoryState ] = useState(new ChatHistoryState());
const [ roomHistoryState, setRoomHistoryState ] = useState(new RoomHistoryState());
const elementRef = useRef<List>(null);
useEffect(() =>
{
const chatState = new ChatHistoryState();
const roomState = new RoomHistoryState();
SetChatHistory(chatState);
chatState.notifier = () => setChatHistoryUpdateId(prevValue => (prevValue + 1));
roomState.notifier = () => setRoomHistoryUpdateId(prevValue => (prevValue + 1));
setChatHistoryState(chatState);
setRoomHistoryState(roomState);
return () => {chatState.notifier = null; roomState.notifier = null;};
}, []);
const onChatHistoryEvent = useCallback((event: ChatHistoryEvent) =>
{
switch(event.type)
{
case ChatHistoryEvent.SHOW_CHAT_HISTORY:
setIsVisible(true);
break;
case ChatHistoryEvent.HIDE_CHAT_HISTORY:
setIsVisible(false);
break;
case ChatHistoryEvent.TOGGLE_CHAT_HISTORY:
setIsVisible(!isVisible);
break;
}
}, [isVisible]);
useUiEvent(ChatHistoryEvent.HIDE_CHAT_HISTORY, onChatHistoryEvent);
useUiEvent(ChatHistoryEvent.SHOW_CHAT_HISTORY, onChatHistoryEvent);
useUiEvent(ChatHistoryEvent.TOGGLE_CHAT_HISTORY, onChatHistoryEvent);
const cache = useMemo(() =>
{
return new CellMeasurerCache({
defaultHeight: 25,
fixedWidth: true,
//keyMapper: (index) => chatHistoryState.chats[index].id
});
}, []);
const RowRenderer: ListRowRenderer = (props: ListRowProps) =>
{
const item = chatHistoryState.chats[props.index];
const isDark = (props.index % 2 === 0);
return (
<CellMeasurer
cache={cache}
columnIndex={0}
key={props.key}
parent={props.parent}
rowIndex={props.index}
>
<div key={props.key} style={props.style} className="chathistory-entry justify-content-start">
{(item.type === ChatEntryType.TYPE_CHAT) &&
<div className={`p-1 d-flex gap-1 ${isDark ? 'dark' : 'light'}`}>
<div className="text-muted">{item.timestamp}</div>
<div className="cursor-pointer d-flex text-nowrap" dangerouslySetInnerHTML={ { __html: (item.name + ':') }} />
<div className="text-break text-wrap flex-grow-1">{item.message}</div>
</div>
}
{(item.type === ChatEntryType.TYPE_ROOM_INFO) &&
<div className={`p-1 d-flex gap-1 ${isDark ? 'dark' : 'light'}`}>
<div className="text-muted">{item.timestamp}</div>
<i className="icon icon-small-room" />
<div className="cursor-pointer text-break text-wrap">{item.name}</div>
</div>
}
</div>
</CellMeasurer>
);
};
const onResize = useCallback((info: Size) =>
{
cache.clearAll();
}, [cache]);
const onRowsRendered = useCallback((info: RenderedRows) =>
{
if(elementRef && elementRef.current && isVisible && needsScroll)
{
console.log('stop ' + info.stopIndex);
//if(chatHistoryState.chats.length > 0) elementRef.current.measureAllRows();
elementRef.current.scrollToRow(chatHistoryState.chats.length);
console.log('scroll')
setNeedsScroll(false);
}
}, [chatHistoryState.chats.length, isVisible, needsScroll]);
useEffect(() =>
{
if(elementRef && elementRef.current && isVisible)
{
//if(chatHistoryState.chats.length > 0) elementRef.current.measureAllRows();
elementRef.current.scrollToRow(chatHistoryState.chats.length);
}
//console.log(chatHistoryState.chats.length);
setNeedsScroll(true);
}, [chatHistoryState.chats, isVisible, chatHistoryUpdateId]);
return (
<ChatHistoryContextProvider value={ { chatHistoryState, roomHistoryState } }>
<ChatHistoryMessageHandler />
{isVisible &&
<NitroCardView uniqueKey="chat-history" className="nitro-chat-history" simple={ false } theme={'dark'} >
<NitroCardHeaderView headerText={ 'Chat History' } onCloseClick={ event => setIsVisible(false) } theme={'dark'}/>
<NitroCardContentView className="chat-history-content p-0" theme={'dark'}>
<div className="row w-100 h-100 chat-history-container">
<AutoSizer defaultWidth={300} defaultHeight={200} onResize={onResize}>
{({ height, width }) =>
{
return (
<List
ref={elementRef}
width={width}
height={height}
rowCount={chatHistoryState.chats.length}
rowHeight={cache.rowHeight}
className={'chat-history-list'}
rowRenderer={RowRenderer}
onRowsRendered={onRowsRendered}
deferredMeasurementCache={cache}
/>
)
}
}
</AutoSizer>
</div>
</NitroCardContentView>
</NitroCardView>
}
</ChatHistoryContextProvider>
);
}

Some files were not shown because too many files have changed in this diff Show More