mirror of
https://github.com/billsonnn/nitro-react.git
synced 2024-11-27 08:00:51 +01:00
Merge branch 'dev' into @update/groups
This commit is contained in:
commit
54312a412e
@ -8,6 +8,7 @@
|
||||
"habbopages.url": "https://website.com/habbopages/",
|
||||
"chat.viewer.height.percentage": 0.40,
|
||||
"widget.dimmer.colorwheel": false,
|
||||
"avatar.wardrobe.max.slots": 10,
|
||||
"hotelview": {
|
||||
"widgets": {
|
||||
"slot.1.widget": "promoarticle",
|
||||
|
@ -70,6 +70,7 @@ $camera-checkout-width: 350px;
|
||||
$room-info-width: 325px;
|
||||
|
||||
$nitro-group-creator-width: 383px;
|
||||
$nitro-mod-tools-width: 175px;
|
||||
|
||||
.nitro-app {
|
||||
width: 100%;
|
||||
|
14
src/App.tsx
14
src/App.tsx
@ -1,13 +1,14 @@
|
||||
import { ConfigurationEvent, HabboWebTools, LegacyExternalInterface, Nitro, NitroCommunicationDemoEvent, NitroEvent, NitroLocalizationEvent, NitroVersion, RoomEngineEvent, WebGL } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useState } from 'react';
|
||||
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 { useLocalizationEvent } from './hooks/events/nitro/localization/localization-event';
|
||||
import { dispatchMainEvent, useMainEvent } from './hooks/events/nitro/main-event';
|
||||
import { useRoomEngineEvent } from './hooks/events/nitro/room/room-engine-event';
|
||||
import { TransitionAnimation, TransitionAnimationTypes } from './layout';
|
||||
import { LoadingView } from './views/loading/LoadingView';
|
||||
import { MainView } from './views/main/MainView';
|
||||
|
||||
export const App: FC<{}> = props =>
|
||||
{
|
||||
@ -127,12 +128,13 @@ export const App: FC<{}> = props =>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="nitro-app overflow-hidden">
|
||||
{ (!isReady || isError) && <LoadingView isError={ isError } message={ message } /> }
|
||||
<Base fit overflow="hidden">
|
||||
{ (!isReady || isError) &&
|
||||
<LoadingView isError={ isError } message={ message } /> }
|
||||
<TransitionAnimation type={ TransitionAnimationTypes.FADE_IN } inProp={ (isReady && !isError) }>
|
||||
<MainView />
|
||||
</TransitionAnimation>
|
||||
<div id="draggable-windows-container" />
|
||||
</div>
|
||||
<Base id="draggable-windows-container" />
|
||||
</Base>
|
||||
);
|
||||
}
|
||||
|
@ -79,6 +79,11 @@
|
||||
font-weight: $font-weight-normal;
|
||||
color: $btn-link-color;
|
||||
text-decoration: $link-decoration;
|
||||
box-shadow: none !important;
|
||||
|
||||
&:active {
|
||||
color: $btn-link-color !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $btn-link-hover-color;
|
||||
|
@ -732,7 +732,7 @@ $table-cell-padding-x-sm: .25rem !default;
|
||||
|
||||
$table-cell-vertical-align: top !default;
|
||||
|
||||
$table-color: $body-color !default;
|
||||
$table-color: $black !default;
|
||||
$table-bg: transparent !default;
|
||||
$table-accent-bg: transparent !default;
|
||||
|
||||
|
@ -90,3 +90,10 @@ ul {
|
||||
.flex-basis-max-content {
|
||||
flex-basis: max-content;
|
||||
}
|
||||
|
||||
.striped-children {
|
||||
|
||||
> :nth-child(1) {
|
||||
background-color: $table-striped-bg;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
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>
|
||||
{
|
||||
@ -11,6 +11,7 @@ export interface BaseProps<T = HTMLElement> extends DetailedHTMLProps<HTMLAttrib
|
||||
fullHeight?: boolean;
|
||||
overflow?: OverflowType;
|
||||
position?: PositionType;
|
||||
float?: FloatType;
|
||||
pointer?: boolean;
|
||||
textColor?: ColorVariantType;
|
||||
classNames?: string[];
|
||||
@ -18,7 +19,7 @@ export interface BaseProps<T = HTMLElement> extends DetailedHTMLProps<HTMLAttrib
|
||||
|
||||
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(() =>
|
||||
{
|
||||
@ -36,6 +37,8 @@ export const Base: FC<BaseProps<HTMLDivElement>> = props =>
|
||||
|
||||
if(position) newClassNames.push('position-' + position);
|
||||
|
||||
if(float) newClassNames.push('float-' + float);
|
||||
|
||||
if(pointer) newClassNames.push('cursor-pointer');
|
||||
|
||||
if(textColor) newClassNames.push('text-' + textColor);
|
||||
@ -43,7 +46,7 @@ export const Base: FC<BaseProps<HTMLDivElement>> = props =>
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
|
||||
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(() =>
|
||||
{
|
||||
@ -51,7 +54,7 @@ export const Base: FC<BaseProps<HTMLDivElement>> = props =>
|
||||
|
||||
if(className.length) newClassName += (' ' + className);
|
||||
|
||||
return newClassName;
|
||||
return newClassName.trim();
|
||||
}, [ getClassNames, className ]);
|
||||
|
||||
const getStyle = useMemo(() =>
|
||||
|
@ -2,7 +2,7 @@ import { FC, useMemo } from 'react';
|
||||
import { CSSProperties } from 'styled-components';
|
||||
import { Base, BaseProps } from './Base';
|
||||
import { GridContextProvider } from './GridContext';
|
||||
import { SpacingType } from './types';
|
||||
import { AlignItemType, AlignSelfType, JustifyContentType, SpacingType } from './types';
|
||||
|
||||
export interface GridProps extends BaseProps<HTMLDivElement>
|
||||
{
|
||||
@ -10,11 +10,15 @@ export interface GridProps extends BaseProps<HTMLDivElement>
|
||||
gap?: SpacingType;
|
||||
maxContent?: boolean;
|
||||
columnCount?: number;
|
||||
center?: boolean;
|
||||
alignSelf?: AlignSelfType;
|
||||
alignItems?: AlignItemType;
|
||||
justifyContent?: JustifyContentType;
|
||||
}
|
||||
|
||||
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(() =>
|
||||
{
|
||||
@ -28,10 +32,18 @@ export const Grid: FC<GridProps> = props =>
|
||||
|
||||
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);
|
||||
|
||||
return newClassNames;
|
||||
}, [ inline, gap, maxContent, classNames ]);
|
||||
}, [ inline, gap, maxContent, alignSelf, alignItems, justifyContent, center, classNames ]);
|
||||
|
||||
const getStyle = useMemo(() =>
|
||||
{
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { FC, useMemo } from 'react';
|
||||
import { Base, BaseProps } from './Base';
|
||||
import { ColorVariantType, FontSizeType, FontWeightType } from './types';
|
||||
import { ColorVariantType, FontSizeType, FontWeightType, TextAlignType } from './types';
|
||||
|
||||
export interface TextProps extends BaseProps<HTMLDivElement>
|
||||
{
|
||||
variant?: ColorVariantType;
|
||||
fontWeight?: FontWeightType;
|
||||
fontSize?: FontSizeType;
|
||||
align?: TextAlignType;
|
||||
bold?: boolean;
|
||||
underline?: boolean;
|
||||
italics?: boolean;
|
||||
@ -14,11 +15,14 @@ export interface TextProps extends BaseProps<HTMLDivElement>
|
||||
center?: boolean;
|
||||
textEnd?: boolean;
|
||||
small?: boolean;
|
||||
wrap?: boolean;
|
||||
noWrap?: boolean;
|
||||
textBreak?: boolean;
|
||||
}
|
||||
|
||||
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(() =>
|
||||
{
|
||||
@ -32,6 +36,8 @@ export const Text: FC<TextProps> = props =>
|
||||
|
||||
if(fontSize) newClassNames.push('fs-' + fontSize);
|
||||
|
||||
if(align) newClassNames.push('text-' + align);
|
||||
|
||||
if(underline) newClassNames.push('text-decoration-underline');
|
||||
|
||||
if(italics) newClassNames.push('fst-italic');
|
||||
@ -44,8 +50,14 @@ export const Text: FC<TextProps> = props =>
|
||||
|
||||
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;
|
||||
}, [ 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 } />;
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
&.active {
|
||||
border-color: $grid-active-border-color !important;
|
||||
background-color: $grid-active-bg-color !important;
|
||||
background-color: $grid-active-bg-color;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
@ -25,4 +25,19 @@
|
||||
.avatar-image {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,12 +14,13 @@ export interface LayoutGridItemProps extends ColumnProps
|
||||
itemUniqueSoldout?: boolean;
|
||||
itemUniqueNumber?: number;
|
||||
itemUnseen?: boolean;
|
||||
itemHighlight?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
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(() =>
|
||||
{
|
||||
@ -33,6 +34,8 @@ export const LayoutGridItem: FC<LayoutGridItemProps> = props =>
|
||||
|
||||
if(itemUnseen) newClassNames.push('unseen');
|
||||
|
||||
if(itemHighlight) newClassNames.push('has-highlight');
|
||||
|
||||
if(disabled) newClassNames.push('disabled')
|
||||
|
||||
if(itemImage === null) newClassNames.push('icon', 'loading-icon');
|
||||
@ -40,7 +43,7 @@ export const LayoutGridItem: FC<LayoutGridItemProps> = props =>
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
|
||||
return newClassNames;
|
||||
}, [ itemActive, itemUniqueSoldout, itemUniqueNumber, itemUnseen, disabled, itemImage, classNames ]);
|
||||
}, [ itemActive, itemUniqueSoldout, itemUniqueNumber, itemUnseen, itemHighlight, disabled, itemImage, classNames ]);
|
||||
|
||||
const getStyle = useMemo(() =>
|
||||
{
|
||||
|
1
src/common/types/FloatType.ts
Normal file
1
src/common/types/FloatType.ts
Normal file
@ -0,0 +1 @@
|
||||
export type FloatType = 'start' | 'end' | 'none';
|
@ -1 +1 @@
|
||||
export type FontSizeType = 1 | 2 | 3 | 4 | 5;
|
||||
export type FontSizeType = 1 | 2 | 3 | 4 | 5 | 6;
|
||||
|
1
src/common/types/TextAlignType.ts
Normal file
1
src/common/types/TextAlignType.ts
Normal file
@ -0,0 +1 @@
|
||||
export type TextAlignType = 'start' | 'center' | 'end';
|
@ -3,9 +3,11 @@ export * from './AlignSelfType';
|
||||
export * from './ButtonSizeType';
|
||||
export * from './ColorVariantType';
|
||||
export * from './ColumnSizesType';
|
||||
export * from './FloatType';
|
||||
export * from './FontSizeType';
|
||||
export * from './FontWeightType';
|
||||
export * from './JustifyContentType';
|
||||
export * from './OverflowType';
|
||||
export * from './PositionType';
|
||||
export * from './SpacingType';
|
||||
export * from './TextAlignType';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { AvatarEditorFigureCategory, FigureSetIdsMessageEvent, GetWardrobeMessageComposer, IAvatarFigureContainer, UserFigureComposer, UserWardrobePageEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { GetAvatarRenderManager, GetClubMemberLevel, GetSessionDataManager, LocalizeText } from '../../api';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { GetAvatarRenderManager, GetClubMemberLevel, GetConfiguration, GetSessionDataManager, LocalizeText } from '../../api';
|
||||
import { Button } from '../../common/Button';
|
||||
import { ButtonGroup } from '../../common/ButtonGroup';
|
||||
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_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 =>
|
||||
{
|
||||
@ -36,13 +35,15 @@ export const AvatarEditorView: FC<{}> = props =>
|
||||
const [ activeCategory, setActiveCategory ] = useState<IAvatarEditorCategoryModel>(null);
|
||||
const [ figureSetIds, setFigureSetIds ] = useState<number[]>([]);
|
||||
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 [ lastFigure, setLastFigure ] = useState<string>(null);
|
||||
const [ lastGender, setLastGender ] = useState<string>(null);
|
||||
const [ needsReset, setNeedsReset ] = useState(false);
|
||||
const [ isInitalized, setIsInitalized ] = useState(false);
|
||||
|
||||
const maxWardrobeSlots = useMemo(() => GetConfiguration<number>('avatar.wardrobe.max.slots', 10), []);
|
||||
|
||||
const onAvatarEditorEvent = useCallback((event: AvatarEditorEvent) =>
|
||||
{
|
||||
switch(event.type)
|
||||
@ -88,7 +89,7 @@ export const AvatarEditorView: FC<{}> = props =>
|
||||
|
||||
let i = 0;
|
||||
|
||||
while(i < MAX_SAVED_FIGURES)
|
||||
while(i < maxWardrobeSlots)
|
||||
{
|
||||
savedFigures.push([ null, null ]);
|
||||
|
||||
@ -103,7 +104,7 @@ export const AvatarEditorView: FC<{}> = props =>
|
||||
}
|
||||
|
||||
setSavedFigures(savedFigures)
|
||||
}, []);
|
||||
}, [ maxWardrobeSlots ]);
|
||||
|
||||
CreateMessageHook(UserWardrobePageEvent, onUserWardrobePageEvent);
|
||||
|
||||
@ -194,6 +195,11 @@ export const AvatarEditorView: FC<{}> = props =>
|
||||
setFigureData(figures.get(gender));
|
||||
}, [ figures ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setSavedFigures(new Array(maxWardrobeSlots));
|
||||
}, [ maxWardrobeSlots ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!isWardrobeVisible) return;
|
||||
|
@ -26,7 +26,7 @@ export const AvatarEditorPaletteSetItem: FC<AvatarEditorPaletteSetItemProps> = p
|
||||
});
|
||||
|
||||
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' } /> }
|
||||
{ children }
|
||||
</LayoutGridItem>
|
||||
|
@ -27,7 +27,7 @@ export const AvatarEditorPaletteSetView: FC<AvatarEditorPaletteSetViewProps> = p
|
||||
}, [ model, category, paletteSet, paletteIndex ]);
|
||||
|
||||
return (
|
||||
<AutoGrid columnCount={ 4 } columnMinWidth={ 30 }>
|
||||
<AutoGrid gap={ 1 } columnCount={ 5 } columnMinWidth={ 30 }>
|
||||
{ (paletteSet.length > 0) && paletteSet.map((item, index) =>
|
||||
<AvatarEditorPaletteSetItem key={ index } colorItem={ item } onClick={ event => selectColor(item) } />) }
|
||||
</AutoGrid>
|
||||
|
@ -44,7 +44,6 @@
|
||||
}
|
||||
|
||||
.nitro-catalog-navigation-grid-container {
|
||||
border-radius: 0.25rem;
|
||||
border-color: #B6BEC5 !important;
|
||||
background-color: #CDD3D9;
|
||||
border: 2px solid;
|
||||
|
@ -20,7 +20,7 @@ export const CatalogNavigationView: FC<CatalogNavigationViewProps> = props =>
|
||||
return (
|
||||
<>
|
||||
<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 }>
|
||||
{ searchResult && (searchResult.filteredNodes.length > 0) && searchResult.filteredNodes.map((n, index) =>
|
||||
{
|
||||
|
@ -13,9 +13,9 @@ import { CatalogEvent } from '../../../../../events/catalog/CatalogEvent';
|
||||
import { useUiEvent } from '../../../../../hooks';
|
||||
import { SendMessageHook } from '../../../../../hooks/messages/message-event';
|
||||
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 { GetCurrencyAmount } from '../../../../purse/common/CurrencyHelper';
|
||||
import { GLOBAL_PURSE } from '../../../../purse/PurseView';
|
||||
import { useCatalogContext } from '../../../CatalogContext';
|
||||
import { CatalogPurchaseState } from '../../../common/CatalogPurchaseState';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
@ -9,7 +9,7 @@ import { Text } from '../../../../../../common/Text';
|
||||
import { BatchUpdates, CreateMessageHook, SendMessageHook } from '../../../../../../hooks';
|
||||
import { NotificationAlertType } from '../../../../../../views/notification-center/common/NotificationAlertType';
|
||||
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 { CatalogLayoutMarketplaceItemView, PUBLIC_OFFER } from './CatalogLayoutMarketplaceItemView';
|
||||
import { SearchFormView } from './CatalogLayoutMarketplaceSearchFormView';
|
||||
|
@ -8,7 +8,7 @@ import { CatalogInitPurchaseEvent } from '../../../../../events/catalog/CatalogI
|
||||
import { CatalogWidgetEvent } from '../../../../../events/catalog/CatalogWidgetEvent';
|
||||
import { dispatchUiEvent, SendMessageHook, useUiEvent } from '../../../../../hooks';
|
||||
import { LoadingSpinnerView } from '../../../../../layout';
|
||||
import { GetCurrencyAmount } from '../../../../../views/purse/common/CurrencyHelper';
|
||||
import { GetCurrencyAmount } from '../../../../purse/common/CurrencyHelper';
|
||||
import { useCatalogContext } from '../../../CatalogContext';
|
||||
import { CatalogPurchaseState } from '../../../common/CatalogPurchaseState';
|
||||
import { Offer } from '../../../common/Offer';
|
||||
|
21
src/components/chat-history/ChatHistoryContext.tsx
Normal file
21
src/components/chat-history/ChatHistoryContext.tsx
Normal 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);
|
@ -2,9 +2,14 @@ import { GetGuestRoomResultEvent, RoomSessionChatEvent, RoomSessionEvent } from
|
||||
import { FC, useCallback, useState } from 'react';
|
||||
import { GetRoomSession } from '../../api';
|
||||
import { CreateMessageHook, useRoomSessionManagerEvent } from '../../hooks';
|
||||
import { useChatHistoryContext } from './context/ChatHistoryContext';
|
||||
import { ChatEntryType, CHAT_HISTORY_MAX, IChatEntry, IRoomHistoryEntry, ROOM_HISTORY_MAX } from './context/ChatHistoryContext.types';
|
||||
import { currentDate } from './utils/Utilities';
|
||||
import { useChatHistoryContext } from './ChatHistoryContext';
|
||||
import { ChatEntryType } from './common/ChatEntryType';
|
||||
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 =>
|
||||
{
|
8
src/components/chat-history/ChatHistoryView.scss
Normal file
8
src/components/chat-history/ChatHistoryView.scss
Normal file
@ -0,0 +1,8 @@
|
||||
.nitro-chat-history {
|
||||
width: $chat-history-width;
|
||||
height: $chat-history-height;
|
||||
|
||||
.content-area {
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
155
src/components/chat-history/ChatHistoryView.tsx
Normal file
155
src/components/chat-history/ChatHistoryView.tsx
Normal 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>
|
||||
);
|
||||
}
|
5
src/components/chat-history/common/ChatEntryType.ts
Normal file
5
src/components/chat-history/common/ChatEntryType.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export class ChatEntryType
|
||||
{
|
||||
public static TYPE_CHAT = 1;
|
||||
public static TYPE_ROOM_INFO = 2;
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { IChatEntry, IChatHistoryState } from '../context/ChatHistoryContext.types';
|
||||
import { IChatEntry } from './IChatEntry';
|
||||
import { IChatHistoryState } from './IChatHistoryState';
|
||||
|
||||
export class ChatHistoryState implements IChatHistoryState
|
||||
{
|
||||
@ -10,6 +11,11 @@ export class ChatHistoryState implements IChatHistoryState
|
||||
this._chats = [];
|
||||
}
|
||||
|
||||
public notify(): void
|
||||
{
|
||||
if(this._notifier) this._notifier();
|
||||
}
|
||||
|
||||
public get chats(): IChatEntry[]
|
||||
{
|
||||
return this._chats;
|
||||
@ -24,9 +30,4 @@ export class ChatHistoryState implements IChatHistoryState
|
||||
{
|
||||
this._notifier = notifier;
|
||||
}
|
||||
|
||||
notify(): void
|
||||
{
|
||||
if(this._notifier) this._notifier();
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { IChatHistoryState } from '../context/ChatHistoryContext.types';
|
||||
import { IChatHistoryState } from './IChatHistoryState';
|
||||
|
||||
let GLOBAL_CHATS: IChatHistoryState = null;
|
||||
|
12
src/components/chat-history/common/IChatEntry.ts
Normal file
12
src/components/chat-history/common/IChatEntry.ts
Normal 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;
|
||||
}
|
8
src/components/chat-history/common/IChatHistoryState.ts
Normal file
8
src/components/chat-history/common/IChatHistoryState.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { IChatEntry } from './IChatEntry';
|
||||
|
||||
export interface IChatHistoryState
|
||||
{
|
||||
chats: IChatEntry[];
|
||||
notifier: () => void
|
||||
notify(): void;
|
||||
}
|
4
src/components/chat-history/common/IRoomHistoryEntry.ts
Normal file
4
src/components/chat-history/common/IRoomHistoryEntry.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface IRoomHistoryEntry {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
8
src/components/chat-history/common/IRoomHistoryState.ts
Normal file
8
src/components/chat-history/common/IRoomHistoryState.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { IRoomHistoryEntry } from './IRoomHistoryEntry';
|
||||
|
||||
export interface IRoomHistoryState
|
||||
{
|
||||
roomHistory: IRoomHistoryEntry[];
|
||||
notifier: () => void;
|
||||
notify: () => void;
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { IRoomHistoryEntry, IRoomHistoryState } from '../context/ChatHistoryContext.types';
|
||||
import { IRoomHistoryEntry } from './IRoomHistoryEntry';
|
||||
import { IRoomHistoryState } from './IRoomHistoryState';
|
||||
|
||||
export class RoomHistoryState implements IRoomHistoryState
|
||||
{
|
@ -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;
|
||||
reportedChats: IChatEntry[];
|
||||
cfhCategory: number;
|
||||
|
@ -2,8 +2,9 @@ import { RoomObjectType } from '@nitrots/nitro-renderer';
|
||||
import { FC, useMemo, useState } from 'react';
|
||||
import { LocalizeText } from '../../../api';
|
||||
import { Button, Column, Flex, Grid, LayoutGridItem, Text } from '../../../common';
|
||||
import { GetChatHistory } from '../../../views/chat-history/common/GetChatHistory';
|
||||
import { ChatEntryType, IChatEntry } from '../../../views/chat-history/context/ChatHistoryContext.types';
|
||||
import { ChatEntryType } from '../../chat-history/common/ChatEntryType';
|
||||
import { GetChatHistory } from '../../chat-history/common/GetChatHistory';
|
||||
import { IChatEntry } from '../../chat-history/common/IChatEntry';
|
||||
import { useHelpContext } from '../HelpContext';
|
||||
|
||||
export const SelectReportedChatsView: FC<{}> = props =>
|
||||
|
@ -2,8 +2,8 @@ import { RoomObjectType } from '@nitrots/nitro-renderer';
|
||||
import { FC, useMemo, useState } from 'react';
|
||||
import { GetSessionDataManager, LocalizeText } from '../../../api';
|
||||
import { Button, Column, Flex, Grid, LayoutGridItem, Text } from '../../../common';
|
||||
import { GetChatHistory } from '../../../views/chat-history/common/GetChatHistory';
|
||||
import { ChatEntryType } from '../../../views/chat-history/context/ChatHistoryContext.types';
|
||||
import { ChatEntryType } from '../../chat-history/common/ChatEntryType';
|
||||
import { GetChatHistory } from '../../chat-history/common/GetChatHistory';
|
||||
import { IReportedUser } from '../common/IReportedUser';
|
||||
import { useHelpContext } from '../HelpContext';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { FC, useMemo, useState } from 'react';
|
||||
import { LocalizeText } from '../../../api';
|
||||
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';
|
||||
|
||||
export const SelectTopicView: FC<{}> = props =>
|
||||
|
@ -2,10 +2,16 @@
|
||||
@import './avatar-editor/AvatarEditorView';
|
||||
@import './camera/CameraWidgetView';
|
||||
@import './catalog/CatalogView';
|
||||
@import './chat-history/ChatHistoryView';
|
||||
@import './groups/GroupView';
|
||||
@import './help/HelpView';
|
||||
@import './inventory/InventoryView';
|
||||
@import './loading/LoadingView';
|
||||
@import './mod-tools/ModToolsView';
|
||||
@import './navigator/NavigatorView';
|
||||
@import './purse/PurseView';
|
||||
@import './right-side/RightSideView';
|
||||
@import './toolbar/ToolbarView';
|
||||
@import './user-profile/UserProfileVew';
|
||||
@import './user-settings/UserSettingsView';
|
||||
@import './wired/WiredView';
|
||||
|
37
src/components/loading/LoadingView.tsx
Normal file
37
src/components/loading/LoadingView.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -1,33 +1,33 @@
|
||||
import { HabboWebTools, RoomSessionEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { AddEventLinkTracker, GetCommunication, RemoveLinkEventTracker } from '../../api';
|
||||
import { AchievementsView } from '../../components/achievements/AchievementsView';
|
||||
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 { Base } from '../../common';
|
||||
import { useRoomSessionManagerEvent } from '../../hooks/events/nitro/session/room-session-manager-event';
|
||||
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 { FloorplanEditorView } from '../floorplan-editor/FloorplanEditorView';
|
||||
import { FriendsView } from '../friends/FriendsView';
|
||||
import { HcCenterView } from '../hc-center/HcCenterView';
|
||||
import { HotelView } from '../hotel-view/HotelView';
|
||||
import { GroupsView } from '../groups/GroupsView';
|
||||
import { HelpView } from '../help/HelpView';
|
||||
import { InventoryView } from '../inventory/InventoryView';
|
||||
import { ModToolsView } from '../mod-tools/ModToolsView';
|
||||
import { NitropediaView } from '../nitropedia/NitropediaView';
|
||||
import { NavigatorView } from '../navigator/NavigatorView';
|
||||
import { RightSideView } from '../right-side/RightSideView';
|
||||
import { RoomHostView } from '../room-host/RoomHostView';
|
||||
import { ToolbarView } from '../toolbar/ToolbarView';
|
||||
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 [ landingViewVisible, setLandingViewVisible ] = useState(true);
|
||||
@ -93,7 +93,7 @@ export const MainView: FC<MainViewProps> = props =>
|
||||
}, [onLinkReceived]);
|
||||
|
||||
return (
|
||||
<div className="nitro-main">
|
||||
<Base fit>
|
||||
<TransitionAnimation type={ TransitionAnimationTypes.FADE_IN } inProp={ landingViewVisible } timeout={ 300 }>
|
||||
<HotelView />
|
||||
</TransitionAnimation>
|
||||
@ -118,6 +118,6 @@ export const MainView: FC<MainViewProps> = props =>
|
||||
<NitropediaView />
|
||||
<HcCenterView />
|
||||
<CampaignView />
|
||||
</div>
|
||||
</Base>
|
||||
);
|
||||
}
|
20
src/components/mod-tools/ModToolsContext.tsx
Normal file
20
src/components/mod-tools/ModToolsContext.tsx
Normal 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);
|
@ -7,10 +7,10 @@ import { ModToolsOpenRoomInfoEvent } from '../../events/mod-tools/ModToolsOpenRo
|
||||
import { ModToolsOpenUserChatlogEvent } from '../../events/mod-tools/ModToolsOpenUserChatlogEvent';
|
||||
import { ModToolsOpenUserInfoEvent } from '../../events/mod-tools/ModToolsOpenUserInfoEvent';
|
||||
import { CreateMessageHook, useRoomEngineEvent, useUiEvent } from '../../hooks';
|
||||
import { NotificationAlertType } from '../notification-center/common/NotificationAlertType';
|
||||
import { NotificationUtilities } from '../notification-center/common/NotificationUtilities';
|
||||
import { NotificationAlertType } from '../../views/notification-center/common/NotificationAlertType';
|
||||
import { NotificationUtilities } from '../../views/notification-center/common/NotificationUtilities';
|
||||
import { SetCfhCategories } from './common/GetCFHCategories';
|
||||
import { useModToolsContext } from './context/ModToolsContext';
|
||||
import { useModToolsContext } from './ModToolsContext';
|
||||
import { ModToolsActions } from './reducers/ModToolsReducer';
|
||||
|
||||
export const ModToolsMessageHandler: FC<{}> = props =>
|
93
src/components/mod-tools/ModToolsView.scss
Normal file
93
src/components/mod-tools/ModToolsView.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { RoomEngineObjectEvent, RoomObjectCategory } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useReducer, useState } from 'react';
|
||||
import { GetRoomSession } from '../../api';
|
||||
import { Button } from '../../common';
|
||||
import { ModToolsEvent } from '../../events/mod-tools/ModToolsEvent';
|
||||
import { ModToolsOpenRoomChatlogEvent } from '../../events/mod-tools/ModToolsOpenRoomChatlogEvent';
|
||||
import { ModToolsOpenRoomInfoEvent } from '../../events/mod-tools/ModToolsOpenRoomInfoEvent';
|
||||
@ -8,24 +10,23 @@ import { ModToolsOpenUserInfoEvent } from '../../events/mod-tools/ModToolsOpenUs
|
||||
import { useRoomEngineEvent } from '../../hooks/events';
|
||||
import { dispatchUiEvent, useUiEvent } from '../../hooks/events/ui/ui-event';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../layout';
|
||||
import { ModToolsContextProvider } from './context/ModToolsContext';
|
||||
import { ModToolsContextProvider } from './ModToolsContext';
|
||||
import { ModToolsMessageHandler } from './ModToolsMessageHandler';
|
||||
import { ModToolsViewProps } from './ModToolsView.types';
|
||||
import { initialModTools, ModToolsActions, ModToolsReducer } from './reducers/ModToolsReducer';
|
||||
import { ISelectedUser } from './utils/ISelectedUser';
|
||||
import { ModToolsChatlogView } from './views/room/room-chatlog/ModToolsChatlogView';
|
||||
import { ModToolsRoomView } from './views/room/room-tools/ModToolsRoomView';
|
||||
import { ModToolsChatlogView } from './views/room/ModToolsChatlogView';
|
||||
import { ModToolsRoomView } from './views/room/ModToolsRoomView';
|
||||
import { ModToolsTicketsView } from './views/tickets/ModToolsTicketsView';
|
||||
import { ModToolsUserChatlogView } from './views/user/user-chatlog/ModToolsUserChatlogView';
|
||||
import { ModToolsUserView } from './views/user/user-info/ModToolsUserView';
|
||||
import { ModToolsUserChatlogView } from './views/user/ModToolsUserChatlogView';
|
||||
import { ModToolsUserView } from './views/user/ModToolsUserView';
|
||||
|
||||
export const ModToolsView: FC<ModToolsViewProps> = props =>
|
||||
export const ModToolsView: FC<{}> = props =>
|
||||
{
|
||||
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 [ 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) =>
|
||||
{
|
||||
@ -191,11 +192,19 @@ export const ModToolsView: FC<ModToolsViewProps> = props =>
|
||||
{ isVisible &&
|
||||
<NitroCardView uniqueKey="mod-tools" className="nitro-mod-tools" simple={ false }>
|
||||
<NitroCardHeaderView headerText={ 'Mod Tools' } onCloseClick={ event => setIsVisible(false) } />
|
||||
<NitroCardContentView className="text-black">
|
||||
<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 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>
|
||||
<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 className="btn btn-primary btn-sm w-100" onClick={ () => setIsTicketsVisible(value => !value) }><i className="fas fa-exclamation-circle"></i> Report Tool</button>
|
||||
<NitroCardContentView className="text-black" gap={ 1 }>
|
||||
<Button gap={ 1 } onClick={ event => handleClick('toggle_room') } disabled={ !currentRoomId }>
|
||||
<FontAwesomeIcon icon="home" /> Room 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>
|
||||
</NitroCardView> }
|
||||
{ openRooms && openRooms.map(roomId =>
|
158
src/components/mod-tools/views/chatlog/ChatlogView.tsx
Normal file
158
src/components/mod-tools/views/chatlog/ChatlogView.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,20 +1,19 @@
|
||||
import { ChatRecordData, GetRoomChatlogMessageComposer, RoomChatlogEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { CreateMessageHook, SendMessageHook } from '../../../../../hooks/messages';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout';
|
||||
import { ChatlogView } from '../../chatlog/ChatlogView';
|
||||
import { ModToolsChatlogViewProps } from './ModToolsChatlogView.types';
|
||||
import { CreateMessageHook, SendMessageHook } from '../../../../hooks/messages';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
|
||||
import { ChatlogView } from '../chatlog/ChatlogView';
|
||||
|
||||
interface ModToolsChatlogViewProps
|
||||
{
|
||||
roomId: number;
|
||||
onCloseClick: () => void;
|
||||
}
|
||||
|
||||
export const ModToolsChatlogView: FC<ModToolsChatlogViewProps> = props =>
|
||||
{
|
||||
const { roomId = null, onCloseClick = null } = props;
|
||||
|
||||
const [roomChatlog, setRoomChatlog] = useState<ChatRecordData>(null);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
SendMessageHook(new GetRoomChatlogMessageComposer(roomId));
|
||||
}, [roomId]);
|
||||
const [ roomChatlog, setRoomChatlog ] = useState<ChatRecordData>(null);
|
||||
|
||||
const onModtoolRoomChatlogEvent = useCallback((event: RoomChatlogEvent) =>
|
||||
{
|
||||
@ -23,17 +22,23 @@ export const ModToolsChatlogView: FC<ModToolsChatlogViewProps> = props =>
|
||||
if(!parser || parser.data.roomId !== roomId) return;
|
||||
|
||||
setRoomChatlog(parser.data);
|
||||
}, [roomId, setRoomChatlog]);
|
||||
}, [ roomId ]);
|
||||
|
||||
CreateMessageHook(RoomChatlogEvent, onModtoolRoomChatlogEvent);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
SendMessageHook(new GetRoomChatlogMessageComposer(roomId));
|
||||
}, [ roomId ]);
|
||||
|
||||
if(!roomChatlog) return null;
|
||||
|
||||
return (
|
||||
<NitroCardView className="nitro-mod-tools-room-chatlog" simple={true}>
|
||||
<NitroCardHeaderView headerText={'Room Chatlog' + (roomChatlog ? ': ' + roomChatlog.roomName : '')} onCloseClick={() => onCloseClick()} />
|
||||
<NitroCardView className="nitro-mod-tools-chatlog" simple={true}>
|
||||
<NitroCardHeaderView headerText={ `Room Chatlog ${ roomChatlog.roomName }` } onCloseClick={ onCloseClick } />
|
||||
<NitroCardContentView className="text-black h-100">
|
||||
{roomChatlog &&
|
||||
<ChatlogView records={[roomChatlog]} />
|
||||
}
|
||||
{ roomChatlog &&
|
||||
<ChatlogView records={ [ roomChatlog ] } /> }
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
128
src/components/mod-tools/views/room/ModToolsRoomView.tsx
Normal file
128
src/components/mod-tools/views/room/ModToolsRoomView.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -1,9 +1,14 @@
|
||||
import { CfhChatlogData, CfhChatlogEvent, GetCfhChatlogMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { CreateMessageHook, SendMessageHook } from '../../../../../hooks';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout';
|
||||
import { ChatlogView } from '../../chatlog/ChatlogView';
|
||||
import { CfhChatlogViewProps } from './CfhChatlogView.types';
|
||||
import { CreateMessageHook, SendMessageHook } from '../../../../hooks';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
|
||||
import { ChatlogView } from '../chatlog/ChatlogView';
|
||||
|
||||
interface CfhChatlogViewProps
|
||||
{
|
||||
issueId: number;
|
||||
onCloseClick(): void;
|
||||
}
|
||||
|
||||
export const CfhChatlogView: FC<CfhChatlogViewProps> = props =>
|
||||
{
|
@ -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) }/> }
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -2,12 +2,16 @@ import { IssueMessageData } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useMemo, useState } from 'react';
|
||||
import { GetSessionDataManager } from '../../../../api';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../layout';
|
||||
import { useModToolsContext } from '../../context/ModToolsContext';
|
||||
import { IssueInfoView } from './issue-info/IssueInfoView';
|
||||
import { ModToolsTicketsViewProps } from './ModToolsTicketsView.types';
|
||||
import { ModToolsMyIssuesTabView } from './my-issues/ModToolsMyIssuesTabView';
|
||||
import { ModToolsOpenIssuesTabView } from './open-issues/ModToolsOpenIssuesTabView';
|
||||
import { ModToolsPickedIssuesTabView } from './picked-issues/ModToolsPickedIssuesTabView';
|
||||
import { useModToolsContext } from '../../ModToolsContext';
|
||||
import { ModToolsIssueInfoView } from './ModToolsIssueInfoView';
|
||||
import { ModToolsMyIssuesTabView } from './ModToolsMyIssuesTabView';
|
||||
import { ModToolsOpenIssuesTabView } from './ModToolsOpenIssuesTabView';
|
||||
import { ModToolsPickedIssuesTabView } from './ModToolsPickedIssuesTabView';
|
||||
|
||||
interface ModToolsTicketsViewProps
|
||||
{
|
||||
onCloseClick: () => void;
|
||||
}
|
||||
|
||||
const TABS: string[] = [
|
||||
'Open Issues',
|
||||
@ -82,25 +86,21 @@ export const ModToolsTicketsView: FC<ModToolsTicketsViewProps> = props =>
|
||||
|
||||
return (
|
||||
<>
|
||||
<NitroCardView className="nitro-mod-tools-tickets" simple={ false }>
|
||||
<NitroCardHeaderView headerText={ 'Tickets' } onCloseClick={ onCloseClick } />
|
||||
<NitroCardContentView className="p-0 text-black">
|
||||
<NitroCardTabsView>
|
||||
{ TABS.map((tab, index) =>
|
||||
{
|
||||
return (<NitroCardTabsItemView key={ index } isActive={ currentTab === index } onClick={ () => setCurrentTab(index) }>
|
||||
{ tab }
|
||||
</NitroCardTabsItemView>);
|
||||
}) }
|
||||
</NitroCardTabsView>
|
||||
<div className="p-2">
|
||||
<NitroCardView className="nitro-mod-tools-tickets">
|
||||
<NitroCardHeaderView headerText={ 'Tickets' } onCloseClick={ onCloseClick } />
|
||||
<NitroCardTabsView>
|
||||
{ TABS.map((tab, index) =>
|
||||
{
|
||||
return (<NitroCardTabsItemView key={ index } isActive={ (currentTab === index) } onClick={ event => setCurrentTab(index) }>
|
||||
{ tab }
|
||||
</NitroCardTabsItemView>);
|
||||
}) }
|
||||
</NitroCardTabsView>
|
||||
<NitroCardContentView gap={ 1 }>
|
||||
<CurrentTabComponent />
|
||||
</div>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
{
|
||||
issueInfoWindows && issueInfoWindows.map(issueId => <IssueInfoView key={issueId} issueId={issueId} onIssueInfoClosed={onIssueInfoClosed}/>)
|
||||
}
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
{ issueInfoWindows && (issueInfoWindows.length > 0) && issueInfoWindows.map(issueId => <ModToolsIssueInfoView key={ issueId } issueId={ issueId } onIssueInfoClosed={ onIssueInfoClosed } />) }
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,20 +1,20 @@
|
||||
import { ChatRecordData, GetUserChatlogMessageComposer, UserChatlogEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { CreateMessageHook, SendMessageHook } from '../../../../../hooks';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout';
|
||||
import { ChatlogView } from '../../chatlog/ChatlogView';
|
||||
import { ModToolsUserChatlogViewProps } from './ModToolsUserChatlogView.types';
|
||||
import { BatchUpdates, CreateMessageHook, SendMessageHook } from '../../../../hooks';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
|
||||
import { ChatlogView } from '../chatlog/ChatlogView';
|
||||
|
||||
interface ModToolsUserChatlogViewProps
|
||||
{
|
||||
userId: number;
|
||||
onCloseClick: () => void;
|
||||
}
|
||||
|
||||
export const ModToolsUserChatlogView: FC<ModToolsUserChatlogViewProps> = props =>
|
||||
{
|
||||
const { userId = null, onCloseClick = null } = props;
|
||||
const [userChatlog, setUserChatlog] = useState<ChatRecordData[]>(null);
|
||||
const [username, setUsername] = useState<string>(null);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
SendMessageHook(new GetUserChatlogMessageComposer(userId));
|
||||
}, [userId]);
|
||||
const [ userChatlog, setUserChatlog ] = useState<ChatRecordData[]>(null);
|
||||
const [ username, setUsername ] = useState<string>(null);
|
||||
|
||||
const onModtoolUserChatlogEvent = useCallback((event: UserChatlogEvent) =>
|
||||
{
|
||||
@ -22,19 +22,26 @@ export const ModToolsUserChatlogView: FC<ModToolsUserChatlogViewProps> = props =
|
||||
|
||||
if(!parser || parser.data.userId !== userId) return;
|
||||
|
||||
setUsername(parser.data.username);
|
||||
setUserChatlog(parser.data.roomChatlogs);
|
||||
}, [setUsername, setUserChatlog, userId]);
|
||||
BatchUpdates(() =>
|
||||
{
|
||||
setUsername(parser.data.username);
|
||||
setUserChatlog(parser.data.roomChatlogs);
|
||||
});
|
||||
}, [ userId ]);
|
||||
|
||||
CreateMessageHook(UserChatlogEvent, onModtoolUserChatlogEvent);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
SendMessageHook(new GetUserChatlogMessageComposer(userId));
|
||||
}, [ userId ]);
|
||||
|
||||
return (
|
||||
<NitroCardView className="nitro-mod-tools-user-chatlog" simple={true}>
|
||||
<NitroCardHeaderView headerText={'User Chatlog' + (username ? ': ' + username : '')} onCloseClick={() => onCloseClick()} />
|
||||
<NitroCardView className="nitro-mod-tools-chatlog" simple>
|
||||
<NitroCardHeaderView headerText={ `User Chatlog: ${ username || '' }` } onCloseClick={ onCloseClick } />
|
||||
<NitroCardContentView className="text-black h-100">
|
||||
{userChatlog &&
|
||||
<ChatlogView records={userChatlog} />
|
||||
}
|
||||
{ userChatlog &&
|
||||
<ChatlogView records={userChatlog} /> }
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -1,13 +1,19 @@
|
||||
import { FriendlyTime, GetModeratorUserInfoMessageComposer, ModeratorUserInfoData, ModeratorUserInfoEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { LocalizeText } from '../../../../../api';
|
||||
import { ModToolsOpenUserChatlogEvent } from '../../../../../events/mod-tools/ModToolsOpenUserChatlogEvent';
|
||||
import { CreateMessageHook, dispatchUiEvent, SendMessageHook } from '../../../../../hooks';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardView, NitroLayoutButton, NitroLayoutGrid, NitroLayoutGridColumn } from '../../../../../layout';
|
||||
import { ModToolsUserModActionView } from '../user-mod-action/ModToolsUserModActionView';
|
||||
import { ModToolsUserRoomVisitsView } from '../user-room-visits/ModToolsUserRoomVisitsView';
|
||||
import { ModToolsSendUserMessageView } from '../user-sendmessage/ModToolsSendUserMessageView';
|
||||
import { ModToolsUserViewProps } from './ModToolsUserView.types';
|
||||
import { LocalizeText } from '../../../../api';
|
||||
import { Button, Column, Grid } from '../../../../common';
|
||||
import { ModToolsOpenUserChatlogEvent } from '../../../../events/mod-tools/ModToolsOpenUserChatlogEvent';
|
||||
import { CreateMessageHook, dispatchUiEvent, SendMessageHook } from '../../../../hooks';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
|
||||
import { ModToolsUserModActionView } from './ModToolsUserModActionView';
|
||||
import { ModToolsUserRoomVisitsView } from './ModToolsUserRoomVisitsView';
|
||||
import { ModToolsUserSendMessageView } from './ModToolsUserSendMessageView';
|
||||
|
||||
interface ModToolsUserViewProps
|
||||
{
|
||||
userId: number;
|
||||
onCloseClick: () => void;
|
||||
}
|
||||
|
||||
export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
|
||||
{
|
||||
@ -17,11 +23,6 @@ export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
|
||||
const [ modActionVisible, setModActionVisible ] = useState(false);
|
||||
const [ roomVisitsVisible, setRoomVisitsVisible ] = useState(false);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
SendMessageHook(new GetModeratorUserInfoMessageComposer(userId));
|
||||
}, [ userId ]);
|
||||
|
||||
const onModtoolUserInfoEvent = useCallback((event: ModeratorUserInfoEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
@ -29,7 +30,7 @@ export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
|
||||
if(!parser || parser.data.userId !== userId) return;
|
||||
|
||||
setUserInfo(parser.data);
|
||||
}, [setUserInfo, userId]);
|
||||
}, [ userId ]);
|
||||
|
||||
CreateMessageHook(ModeratorUserInfoEvent, onModtoolUserInfoEvent);
|
||||
|
||||
@ -98,52 +99,58 @@ export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
|
||||
];
|
||||
}, [ userInfo ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
SendMessageHook(new GetModeratorUserInfoMessageComposer(userId));
|
||||
}, [ userId ]);
|
||||
|
||||
if(!userInfo) return null;
|
||||
|
||||
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() } />
|
||||
<NitroCardContentView className="text-black">
|
||||
<NitroLayoutGrid>
|
||||
<NitroLayoutGridColumn size={ 8 }>
|
||||
<Grid overflow="hidden">
|
||||
<Column size={ 8 } overflow="auto">
|
||||
<table className="table table-striped table-sm table-text-small text-black m-0">
|
||||
<tbody>
|
||||
{ userProperties.map( (property, index) =>
|
||||
{
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
<tr key={ index }>
|
||||
<th scope="row">{ LocalizeText(property.localeKey) }</th>
|
||||
<td>
|
||||
{ property.value }
|
||||
{ property.showOnline && <i className={ `icon icon-pf-${ userInfo.online ? 'online' : 'offline' } ms-2` } /> }
|
||||
</td>
|
||||
{ property.showOnline &&
|
||||
<i className={ `icon icon-pf-${ userInfo.online ? 'online' : 'offline' } ms-2` } /> }
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}) }
|
||||
</tbody>
|
||||
</table>
|
||||
</NitroLayoutGridColumn>
|
||||
<NitroLayoutGridColumn size={ 4 }>
|
||||
<NitroLayoutButton variant="primary" size="sm" onClick={ event => dispatchUiEvent(new ModToolsOpenUserChatlogEvent(userId)) }>
|
||||
</Column>
|
||||
<Column size={ 4 } gap={ 1 }>
|
||||
<Button onClick={ event => dispatchUiEvent(new ModToolsOpenUserChatlogEvent(userId)) }>
|
||||
Room Chat
|
||||
</NitroLayoutButton>
|
||||
<NitroLayoutButton variant="primary" size="sm" onClick={ event => setSendMessageVisible(!sendMessageVisible) }>
|
||||
</Button>
|
||||
<Button onClick={ event => setSendMessageVisible(!sendMessageVisible) }>
|
||||
Send Message
|
||||
</NitroLayoutButton>
|
||||
<NitroLayoutButton variant="primary" size="sm" onClick={ event => setRoomVisitsVisible(!roomVisitsVisible) }>
|
||||
</Button>
|
||||
<Button onClick={ event => setRoomVisitsVisible(!roomVisitsVisible) }>
|
||||
Room Visits
|
||||
</NitroLayoutButton>
|
||||
<NitroLayoutButton variant="primary" size="sm" onClick={ event => setModActionVisible(!modActionVisible) }>
|
||||
</Button>
|
||||
<Button onClick={ event => setModActionVisible(!modActionVisible) }>
|
||||
Mod Action
|
||||
</NitroLayoutButton>
|
||||
</NitroLayoutGridColumn>
|
||||
</NitroLayoutGrid>
|
||||
</Button>
|
||||
</Column>
|
||||
</Grid>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
{ sendMessageVisible &&
|
||||
<ModToolsSendUserMessageView user={ { userId: userId, username: userInfo.userName } } onCloseClick={ () => setSendMessageVisible(false) } /> }
|
||||
<ModToolsUserSendMessageView user={ { userId: userId, username: userInfo.userName } } onCloseClick={ () => setSendMessageVisible(false) } /> }
|
||||
{ modActionVisible &&
|
||||
<ModToolsUserModActionView user={ { userId: userId, username: userInfo.userName } } onCloseClick={ () => setModActionVisible(false) } /> }
|
||||
{ roomVisitsVisible &&
|
@ -1,11 +1,16 @@
|
||||
import { createContext, FC, useContext } from 'react';
|
||||
import { IPurseContext, PurseContextProps } from './PurseContext.types';
|
||||
import { createContext, FC, ProviderProps, useContext } from 'react';
|
||||
import { IPurse } from './common/IPurse';
|
||||
|
||||
interface IPurseContext
|
||||
{
|
||||
purse: IPurse;
|
||||
}
|
||||
|
||||
const PurseContext = createContext<IPurseContext>({
|
||||
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>
|
||||
}
|
@ -2,10 +2,9 @@ import { ActivityPointNotificationMessageEvent, UserCreditsEvent, UserCurrencyEv
|
||||
import { FC, useCallback } from 'react';
|
||||
import { CREDITS, DUCKETS, PlaySound } from '../../api/utils/PlaySound';
|
||||
import { CreateMessageHook } from '../../hooks/messages/message-event';
|
||||
import { usePurseContext } from './context/PurseContext';
|
||||
import { PurseMessageHandlerProps } from './PurseMessageHandler.types';
|
||||
import { usePurseContext } from './PurseContext';
|
||||
|
||||
export const PurseMessageHandler: FC<PurseMessageHandlerProps> = props =>
|
||||
export const PurseMessageHandler: FC<{}> = props =>
|
||||
{
|
||||
const { purse = null } = usePurseContext();
|
||||
|
26
src/components/purse/PurseView.scss
Normal file
26
src/components/purse/PurseView.scss
Normal 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);
|
||||
}
|
||||
}
|
137
src/components/purse/PurseView.tsx
Normal file
137
src/components/purse/PurseView.tsx
Normal 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>
|
||||
);
|
||||
}
|
40
src/components/purse/views/CurrencyView.tsx
Normal file
40
src/components/purse/views/CurrencyView.tsx
Normal 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>
|
||||
);
|
||||
}
|
25
src/components/purse/views/SeasonalView.tsx
Normal file
25
src/components/purse/views/SeasonalView.tsx
Normal 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>
|
||||
);
|
||||
}
|
18
src/components/right-side/RightSideView.tsx
Normal file
18
src/components/right-side/RightSideView.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
import { IRoomSession, RoomEngineEvent, RoomId, RoomSessionEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useState } from 'react';
|
||||
import { GetRoomSession, SetActiveRoomId, StartRoomSession } from '../../api';
|
||||
import { Base } from '../../common';
|
||||
import { useRoomEngineEvent } from '../../hooks/events/nitro/room/room-engine-event';
|
||||
import { useRoomSessionManagerEvent } from '../../hooks/events/nitro/session/room-session-manager-event';
|
||||
import { TransitionAnimation, TransitionAnimationTypes } from '../../layout';
|
||||
import { RoomView } from '../room/RoomView';
|
||||
import { RoomView } from '../../views/room/RoomView';
|
||||
|
||||
export const RoomHostView: FC<{}> = props =>
|
||||
{
|
||||
@ -51,9 +52,9 @@ export const RoomHostView: FC<{}> = props =>
|
||||
|
||||
return (
|
||||
<TransitionAnimation type={ TransitionAnimationTypes.FADE_IN } inProp={ !!roomSession } timeout={ 300 }>
|
||||
<div className="nitro-room-host w-100 h-100">
|
||||
<Base fit>
|
||||
<RoomView roomSession={ roomSession } />
|
||||
</div>
|
||||
</Base>
|
||||
</TransitionAnimation>
|
||||
);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { RoomObjectCategory } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect } from 'react';
|
||||
import { GetRoomEngine, GetRoomSession } from '../../api';
|
||||
import { Base, Flex } from '../../common';
|
||||
import { ItemCountView } from '../../views/shared/item-count/ItemCountView';
|
||||
import { ToolbarViewItems } from './common/ToolbarViewItems';
|
||||
|
||||
@ -24,35 +25,15 @@ export const ToolbarMeView: FC<ToolbarMeViewProps> = props =>
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="d-flex nitro-toolbar-me px-1 py-2">
|
||||
<div className="navigation-items">
|
||||
<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) &&
|
||||
<ItemCountView count={ unseenAchievementCount } /> }
|
||||
</div>
|
||||
<div className="navigation-item" onClick={ () => handleToolbarItemClick(ToolbarViewItems.PROFILE_ITEM) }>
|
||||
<i className="icon icon-me-profile"></i>
|
||||
</div>
|
||||
<div className="navigation-item">
|
||||
<i className="icon icon-me-rooms"></i>
|
||||
</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>
|
||||
<Flex alignItems="center" className="nitro-toolbar-me p-2" gap={ 2 }>
|
||||
<Base pointer className="navigation-item icon icon-me-achievements" onClick={ () => handleToolbarItemClick(ToolbarViewItems.ACHIEVEMENTS_ITEM) }>
|
||||
{ (unseenAchievementCount > 0) &&
|
||||
<ItemCountView count={ unseenAchievementCount } /> }
|
||||
</Base>
|
||||
<Base pointer className="navigation-item icon icon-me-profile" onClick={ () => handleToolbarItemClick(ToolbarViewItems.PROFILE_ITEM) } />
|
||||
<Base pointer className="navigation-item icon icon-me-rooms" />
|
||||
<Base pointer className="navigation-item icon icon-me-clothing" onClick={ () => handleToolbarItemClick(ToolbarViewItems.CLOTHING_ITEM) } />
|
||||
<Base pointer className="navigation-item icon icon-me-settings" onClick={ () => handleToolbarItemClick(ToolbarViewItems.SETTINGS_ITEM) } />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
@ -1,111 +1,67 @@
|
||||
.nitro-toolbar-container {
|
||||
.nitro-toolbar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: $toolbar-height;
|
||||
z-index: $toolbar-zindex;
|
||||
pointer-events: all;
|
||||
background: 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-toolbar {
|
||||
height: 100%;
|
||||
pointer-events: all;
|
||||
background: 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);
|
||||
.navigation-item {
|
||||
|
||||
#toolbar-chat-input-container {
|
||||
margin: 0 10px;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
&.item-avatar {
|
||||
width: 50px;
|
||||
height: 45px;
|
||||
overflow: hidden;
|
||||
|
||||
.avatar-image {
|
||||
margin-left: -5px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.navigation-avatar {
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.navigation-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
//margin: 0 1px;
|
||||
position: relative;
|
||||
|
||||
&.item-avatar {
|
||||
width: 50px;
|
||||
height: 45px;
|
||||
overflow: hidden;
|
||||
|
||||
.avatar-image {
|
||||
margin-left: -5px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon,
|
||||
&.item-avatar {
|
||||
position: relative;
|
||||
//transition: transform .2s ease-out;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
-webkit-transform: translate(-1px, -1px);
|
||||
transform: translate(-1px, -1px);
|
||||
filter: drop-shadow(2px 2px 0 rgba($black, 0.8));
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
left: 60px;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
-webkit-transform: translate(-1px, -1px);
|
||||
transform: translate(-1px, -1px);
|
||||
filter: drop-shadow(2px 2px 0 rgba($black, 0.8));
|
||||
}
|
||||
|
||||
.nitro-toolbar-me-menu {
|
||||
bottom: 77px;
|
||||
left: 200px;
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
z-index: $toolbar-memenu-zindex;
|
||||
&.active,
|
||||
&:active {
|
||||
-webkit-transform: translate(0px, 0px);
|
||||
transform: translate(0px, 0px);
|
||||
filter: drop-shadow(2px 2px 0 rgba($black, 0.8));
|
||||
}
|
||||
}
|
||||
|
||||
.list-group {
|
||||
.list-group-item {
|
||||
min-width: 70px;
|
||||
transition: all 0.3s;
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
#toolbar-chat-input-container {
|
||||
|
||||
i {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
@include media-breakpoint-down(sm) {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $cyan;
|
||||
text-decoration: underline;
|
||||
.nitro-toolbar-me {
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
left: 15px;
|
||||
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;
|
||||
|
||||
i {
|
||||
filter: grayscale(0);
|
||||
}
|
||||
}
|
||||
.navigation-item {
|
||||
transition: filter .2s ease-out;
|
||||
filter: grayscale(1);
|
||||
|
||||
.count {
|
||||
top: 0px;
|
||||
right: 5px;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
filter: grayscale(0) drop-shadow(2px 2px 0 rgba($black, 0.8));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -122,45 +78,3 @@
|
||||
drop-shadow(-2px 1px 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Dispose, DropBounce, EaseOut, FigureUpdateEvent, JumpBy, Motions, NitroToolbarAnimateIconEvent, Queue, UserInfoDataParser, UserInfoEvent, Wait } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useState } from 'react';
|
||||
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 { AchievementsUIEvent, AchievementsUIUnseenCountEvent } from '../../events/achievements';
|
||||
import { UnseenItemTrackerUpdateEvent } from '../../events/inventory/UnseenItemTrackerUpdateEvent';
|
||||
@ -181,64 +182,47 @@ export const ToolbarView: FC<ToolbarViewProps> = props =>
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="nitro-toolbar-container">
|
||||
<>
|
||||
<TransitionAnimation type={ TransitionAnimationTypes.FADE_IN } inProp={ isMeExpanded } timeout={ 300 }>
|
||||
<ToolbarMeView unseenAchievementCount={ unseenAchievementCount } handleToolbarItemClick={ handleToolbarItemClick } />
|
||||
</TransitionAnimation>
|
||||
<div className="d-flex justify-content-between align-items-center nitro-toolbar py-1 px-3">
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="navigation-items gap-2">
|
||||
<div className={ 'navigation-item item-avatar ' + (isMeExpanded ? 'active ' : '') } onClick={ event => setMeExpanded(!isMeExpanded) }>
|
||||
<Flex alignItems="center" justifyContent="between" gap={ 2 } className="nitro-toolbar py-1 px-3">
|
||||
<Flex gap={ 2 } alignItems="center">
|
||||
<Flex alignItems="center" gap={ 2 }>
|
||||
<Flex center pointer className={ 'navigation-item item-avatar ' + (isMeExpanded ? 'active ' : '') } onClick={ event => setMeExpanded(!isMeExpanded) }>
|
||||
<AvatarImageView figure={ userFigure } direction={ 2 } />
|
||||
{ (unseenAchievementCount > 0) &&
|
||||
<ItemCountView count={ unseenAchievementCount } /> }
|
||||
</div>
|
||||
{ isInRoom && (
|
||||
<div className="navigation-item" onClick={ visitDesktop }>
|
||||
<i className="icon icon-habbo"></i>
|
||||
</div>) }
|
||||
{ !isInRoom && (
|
||||
<div className="navigation-item" onClick={ event => CreateLinkEvent('navigator/goto/home') }>
|
||||
<i className="icon icon-house"></i>
|
||||
</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>
|
||||
</Flex>
|
||||
{ isInRoom &&
|
||||
<Base pointer className="navigation-item icon icon-habbo" onClick={ visitDesktop } /> }
|
||||
{ !isInRoom &&
|
||||
<Base pointer className="navigation-item icon icon-house" onClick={ event => CreateLinkEvent('navigator/goto/home') } /> }
|
||||
<Base pointer className="navigation-item icon icon-rooms" onClick={ event => handleToolbarItemClick(ToolbarViewItems.NAVIGATOR_ITEM) } />
|
||||
<Base pointer className="navigation-item icon icon-catalog" onClick={ event => handleToolbarItemClick(ToolbarViewItems.CATALOG_ITEM) } />
|
||||
<Base pointer className="navigation-item icon icon-inventory" onClick={ event => handleToolbarItemClick(ToolbarViewItems.INVENTORY_ITEM) }>
|
||||
{ (unseenInventoryCount > 0) &&
|
||||
<ItemCountView count={ unseenInventoryCount } /> }
|
||||
</div>
|
||||
{ isInRoom && (
|
||||
<div className="navigation-item" onClick={ event => handleToolbarItemClick(ToolbarViewItems.CAMERA_ITEM) }>
|
||||
<i className="icon icon-camera"></i>
|
||||
</div>) }
|
||||
{ isMod && (
|
||||
<div className="navigation-item" onClick={ event => handleToolbarItemClick(ToolbarViewItems.MOD_TOOLS_ITEM) }>
|
||||
<i className="icon icon-modtools"></i>
|
||||
</div>) }
|
||||
</div>
|
||||
<div id="toolbar-chat-input-container" className="d-flex align-items-center" />
|
||||
</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>
|
||||
</Base>
|
||||
{ isInRoom &&
|
||||
<Base pointer className="navigation-item icon icon-camera" onClick={ event => handleToolbarItemClick(ToolbarViewItems.CAMERA_ITEM) } /> }
|
||||
{ isMod &&
|
||||
<Base pointer className="navigation-item icon icon-modtools" onClick={ event => handleToolbarItemClick(ToolbarViewItems.MOD_TOOLS_ITEM) } /> }
|
||||
</Flex>
|
||||
<Flex alignItems="center" id="toolbar-chat-input-container" />
|
||||
</Flex>
|
||||
<Flex alignItems="center" gap={ 2 }>
|
||||
<Flex gap={ 2 }>
|
||||
<Base pointer className="navigation-item icon icon-friendall" onClick={ event => handleToolbarItemClick(ToolbarViewItems.FRIEND_LIST_ITEM) }>
|
||||
{ (unseenFriendRequestCount > 0) &&
|
||||
<ItemCountView count={ unseenFriendRequestCount } /> }
|
||||
</div>
|
||||
</Base>
|
||||
{ ((chatIconType === CHAT_ICON_SHOWING) || (chatIconType === CHAT_ICON_UNREAD)) &&
|
||||
<div className="navigation-item" onClick={ event => handleToolbarItemClick(ToolbarViewItems.FRIEND_CHAT_ITEM) }>
|
||||
{ (chatIconType === CHAT_ICON_SHOWING) && <i className="icon icon-message" /> }
|
||||
{ (chatIconType === CHAT_ICON_UNREAD) && <i className="icon icon-message is-unseen" /> }
|
||||
</div> }
|
||||
</div>
|
||||
<div id="toolbar-friend-bar-container" className="d-none d-lg-block" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Base pointer className={ `navigation-item icon icon-message ${ (chatIconType === CHAT_ICON_UNREAD) && 'is-unread' }` } onClick={ event => handleToolbarItemClick(ToolbarViewItems.FRIEND_CHAT_ITEM) } /> }
|
||||
</Flex>
|
||||
<Base id="toolbar-friend-bar-container" className="d-none d-lg-block" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,3 @@
|
||||
export interface ToolbarViewProps
|
||||
{
|
||||
isInRoom: boolean;
|
||||
}
|
||||
|
||||
export class ToolbarViewItems
|
||||
{
|
||||
public static NAVIGATOR_ITEM: string = 'TVI_NAVIGATOR_ITEM';
|
||||
|
106
src/components/user-profile/UserProfileVew.scss
Normal file
106
src/components/user-profile/UserProfileVew.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
104
src/components/user-profile/UserProfileView.tsx
Normal file
104
src/components/user-profile/UserProfileView.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -1,7 +1,11 @@
|
||||
import { FC } from 'react';
|
||||
import { NitroCardGridItemView, NitroCardGridView } from '../../../../layout';
|
||||
import { BadgeImageView } from '../../../shared/badge-image/BadgeImageView';
|
||||
import { BadgesContainerViewProps } from './BadgesContainerView.types';
|
||||
import { NitroCardGridItemView, NitroCardGridView } from '../../../layout';
|
||||
import { BadgeImageView } from '../../../views/shared/badge-image/BadgeImageView';
|
||||
|
||||
interface BadgesContainerViewProps
|
||||
{
|
||||
badges: string[];
|
||||
}
|
||||
|
||||
export const BadgesContainerView: FC<BadgesContainerViewProps> = props =>
|
||||
{
|
@ -1,7 +1,13 @@
|
||||
import { RelationshipStatusInfoMessageParser } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { LocalizeText } from '../../../../api';
|
||||
import { RelationshipsContainerView } from '../relationships-container/RelationshipsContainerView';
|
||||
import { FriendsContainerViewProps } from './FriendsContainerView.types';
|
||||
import { LocalizeText } from '../../../api';
|
||||
import { RelationshipsContainerView } from './RelationshipsContainerView';
|
||||
|
||||
interface FriendsContainerViewProps
|
||||
{
|
||||
relationships: RelationshipStatusInfoMessageParser;
|
||||
friendsCount: number;
|
||||
}
|
||||
|
||||
export const FriendsContainerView: FC<FriendsContainerViewProps> = props =>
|
||||
{
|
@ -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 { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { GroupInformationView } from '../../../../components/groups/views/information/GroupInformationView';
|
||||
import { CreateMessageHook, SendMessageHook } from '../../../../hooks';
|
||||
import { BadgeImageView } from '../../../shared/badge-image/BadgeImageView';
|
||||
import { GroupsContainerViewProps } from './GroupsContainerView.types';
|
||||
import { CreateMessageHook, SendMessageHook } from '../../../hooks';
|
||||
import { BadgeImageView } from '../../../views/shared/badge-image/BadgeImageView';
|
||||
import { GroupInformationView } from '../../groups/views/information/GroupInformationView';
|
||||
|
||||
interface GroupsContainerViewProps
|
||||
{
|
||||
itsMe: boolean;
|
||||
groups: HabboGroupEntryData[];
|
||||
onLeaveGroup: () => void;
|
||||
}
|
||||
|
||||
export const GroupsContainerView: FC<GroupsContainerViewProps> = props =>
|
||||
{
|
@ -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 { GetUserProfile, LocalizeText } from '../../../../api';
|
||||
import { AvatarImageView } from '../../../shared/avatar-image/AvatarImageView';
|
||||
import { RelationshipsContainerViewProps } from './RelationshipsContainerView.types';
|
||||
import { GetUserProfile, LocalizeText } from '../../../api';
|
||||
import { AvatarImageView } from '../../../views/shared/avatar-image/AvatarImageView';
|
||||
|
||||
interface RelationshipsContainerViewProps
|
||||
{
|
||||
relationships: RelationshipStatusInfoMessageParser;
|
||||
simple?: boolean;
|
||||
}
|
||||
|
||||
export const RelationshipsContainerView: FC<RelationshipsContainerViewProps> = props =>
|
||||
{
|
@ -1,8 +1,12 @@
|
||||
import { FriendlyTime } from '@nitrots/nitro-renderer';
|
||||
import { FriendlyTime, UserProfileParser } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback } from 'react';
|
||||
import { GetSessionDataManager, LocalizeText } from '../../../../api';
|
||||
import { AvatarImageView } from '../../../shared/avatar-image/AvatarImageView';
|
||||
import { UserContainerViewProps } from './UserContainerView.types';
|
||||
import { GetSessionDataManager, LocalizeText } from '../../../api';
|
||||
import { AvatarImageView } from '../../../views/shared/avatar-image/AvatarImageView';
|
||||
|
||||
interface UserContainerViewProps
|
||||
{
|
||||
userProfile: UserProfileParser;
|
||||
}
|
||||
|
||||
export const UserContainerView: FC<UserContainerViewProps> = props =>
|
||||
{
|
@ -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';
|
||||
}
|
@ -9,6 +9,7 @@ body {
|
||||
user-select: none;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: -moz-crisp-edges;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
img {
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { FC, useMemo } from 'react';
|
||||
import { Column } from '../../../common/Column';
|
||||
import { Column, ColumnProps } from '../../../common';
|
||||
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 { simple = false } = useNitroCardContext();
|
||||
const { classNames = [], ...rest } = props;
|
||||
const { theme = 'primary', simple = false } = useNitroCardContext();
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
|
@ -1,7 +0,0 @@
|
||||
import { ColumnProps } from '../../../common/Column';
|
||||
|
||||
|
||||
export interface NitroCardContentViewProps extends ColumnProps
|
||||
{
|
||||
theme?: string;
|
||||
}
|
@ -1,2 +1 @@
|
||||
export * from './NitroCardContentView';
|
||||
export * from './NitroCardContextView.types';
|
||||
|
@ -5,8 +5,8 @@ import { NitroCardHeaderViewProps } from './NitroCardHeaderView.types';
|
||||
|
||||
export const NitroCardHeaderView: FC<NitroCardHeaderViewProps> = props =>
|
||||
{
|
||||
const { headerText = null, onCloseClick = null, theme = 'primary' } = props;
|
||||
const { simple = false } = useNitroCardContext();
|
||||
const { headerText = null, onCloseClick = null } = props;
|
||||
const { theme = 'primary', simple = false } = useNitroCardContext();
|
||||
|
||||
const onMouseDown = useCallback((event: MouseEvent<HTMLDivElement>) =>
|
||||
{
|
||||
|
@ -1,16 +1,8 @@
|
||||
@import "./shared/Shared";
|
||||
@import "./friends/FriendsView";
|
||||
@import "./hotel-view/HotelView";
|
||||
@import "./loading/LoadingView";
|
||||
@import "./main/MainView";
|
||||
@import "./notification-center/NotificationCenterView";
|
||||
@import "./purse/PurseView";
|
||||
@import "./right-side/RightSideView";
|
||||
@import "./room/RoomView";
|
||||
@import "./room-host/RoomHostView";
|
||||
@import "./mod-tools/ModToolsView";
|
||||
@import "./user-profile/UserProfileVew";
|
||||
@import "./chat-history/ChatHistoryView";
|
||||
@import "./floorplan-editor/FloorplanEditorView";
|
||||
@import "./nitropedia/NitropediaView";
|
||||
@import "./hc-center/HcCenterView.scss";
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user