Merge branch 'dev' into feature/habbopages

This commit is contained in:
dank074 2021-11-18 02:46:30 -06:00
commit 97b8454c06
81 changed files with 1362 additions and 404 deletions

View File

@ -0,0 +1,110 @@
import { PollQuestion } from '@nitrots/nitro-renderer';
import { RoomWidgetUpdateEvent } from './RoomWidgetUpdateEvent';
export class RoomWidgetPollUpdateEvent extends RoomWidgetUpdateEvent
{
public static readonly OFFER = 'RWPUW_OFFER';
public static readonly ERROR = 'RWPUW_ERROR';
public static readonly CONTENT = 'RWPUW_CONTENT';
private _id = -1;
private _summary: string;
private _headline: string;
private _numQuestions = 0;
private _startMessage = '';
private _endMessage = '';
private _questionArray: PollQuestion[] = null;
private _pollType = '';
private _npsPoll = false;
constructor(type: string, id: number)
{
super(type);
this._id = id;
}
public get id(): number
{
return this._id;
}
public get summary(): string
{
return this._summary;
}
public set summary(k: string)
{
this._summary = k;
}
public get headline(): string
{
return this._headline;
}
public set headline(k: string)
{
this._headline = k;
}
public get numQuestions(): number
{
return this._numQuestions;
}
public set numQuestions(k: number)
{
this._numQuestions = k;
}
public get startMessage(): string
{
return this._startMessage;
}
public set startMessage(k: string)
{
this._startMessage = k;
}
public get endMessage(): string
{
return this._endMessage;
}
public set endMessage(k: string)
{
this._endMessage = k;
}
public get questionArray(): PollQuestion[]
{
return this._questionArray;
}
public set questionArray(k: PollQuestion[])
{
this._questionArray = k;
}
public get pollType(): string
{
return this._pollType;
}
public set pollType(k: string)
{
this._pollType = k;
}
public get npsPoll(): boolean
{
return this._npsPoll;
}
public set npsPoll(k: boolean)
{
this._npsPoll = k;
}
}

View File

@ -0,0 +1,110 @@
import { IQuestion } from '@nitrots/nitro-renderer';
import { RoomWidgetUpdateEvent } from './RoomWidgetUpdateEvent';
export class RoomWidgetWordQuizUpdateEvent extends RoomWidgetUpdateEvent
{
public static readonly NEW_QUESTION = 'RWPUW_NEW_QUESTION';
public static readonly QUESTION_FINISHED = 'RWPUW_QUESION_FINSIHED';
public static readonly QUESTION_ANSWERED = 'RWPUW_QUESTION_ANSWERED';
private _id: number = -1;
private _pollType: string = null;
private _pollId: number = -1;
private _questionId: number = -1;
private _duration: number = -1;
private _question: IQuestion = null;
private _userId: number = -1;
private _value: string;
private _answerCounts: Map<string, number>;
constructor(type: string, id: number)
{
super(type);
this._id = id;
}
public get id(): number
{
return this._id;
}
public get pollType(): string
{
return this._pollType;
}
public set pollType(k: string)
{
this._pollType = k;
}
public get pollId(): number
{
return this._pollId;
}
public set pollId(k: number)
{
this._pollId = k;
}
public get questionId(): number
{
return this._questionId;
}
public set questionId(k: number)
{
this._questionId = k;
}
public get duration(): number
{
return this._duration;
}
public set duration(k: number)
{
this._duration = k;
}
public get question(): IQuestion
{
return this._question;
}
public set question(k: IQuestion)
{
this._question = k;
}
public get userId(): number
{
return this._userId;
}
public set userId(k: number)
{
this._userId = k;
}
public get value(): string
{
return this._value;
}
public set value(k: string)
{
this._value = k;
}
public get answerCounts(): Map<string, number>
{
return this._answerCounts;
}
public set answerCounts(k: Map<string, number>)
{
this._answerCounts = k;
}
}

View File

@ -0,0 +1,75 @@
import { NitroEvent, RoomSessionPollEvent, RoomWidgetEnum } from '@nitrots/nitro-renderer';
import { RoomWidgetPollUpdateEvent } from '../events/RoomWidgetPollUpdateEvent';
import { RoomWidgetUpdateEvent } from '../events/RoomWidgetUpdateEvent';
import { RoomWidgetMessage } from '../messages/RoomWidgetMessage';
import { RoomWidgetPollMessage } from '../messages/RoomWidgetPollMessage';
import { RoomWidgetHandler } from './RoomWidgetHandler';
export class PollWidgetHandler extends RoomWidgetHandler
{
public processEvent(event: NitroEvent): void
{
const pollEvent = (event as RoomSessionPollEvent);
let widgetEvent: RoomWidgetPollUpdateEvent;
switch(event.type)
{
case RoomSessionPollEvent.OFFER:
widgetEvent = new RoomWidgetPollUpdateEvent(RoomWidgetPollUpdateEvent.OFFER, pollEvent.id);
widgetEvent.summary = pollEvent.summary;
widgetEvent.headline = pollEvent.headline;
break;
case RoomSessionPollEvent.ERROR:
widgetEvent = new RoomWidgetPollUpdateEvent(RoomWidgetPollUpdateEvent.ERROR, pollEvent.id);
widgetEvent.summary = pollEvent.summary;
widgetEvent.headline = pollEvent.headline;
break;
case RoomSessionPollEvent.CONTENT:
widgetEvent = new RoomWidgetPollUpdateEvent(RoomWidgetPollUpdateEvent.CONTENT, pollEvent.id);
widgetEvent.startMessage = pollEvent.startMessage;
widgetEvent.endMessage = pollEvent.endMessage;
widgetEvent.numQuestions = pollEvent.numQuestions;
widgetEvent.questionArray = pollEvent.questionArray;
widgetEvent.npsPoll = pollEvent.npsPoll;
break;
}
if(!widgetEvent) return;
this.container.eventDispatcher.dispatchEvent(widgetEvent);
}
public processWidgetMessage(message: RoomWidgetMessage): RoomWidgetUpdateEvent
{
const pollMessage = (message as RoomWidgetPollMessage);
switch(message.type)
{
case RoomWidgetPollMessage.START:
this.container.roomSession.sendPollStartMessage(pollMessage.id);
break;
case RoomWidgetPollMessage.REJECT:
this.container.roomSession.sendPollRejectMessage(pollMessage.id);
break;
case RoomWidgetPollMessage.ANSWER:
this.container.roomSession.sendPollAnswerMessage(pollMessage.id, pollMessage.questionId, pollMessage.answers);
break;
}
return null;
}
public get type(): string
{
return RoomWidgetEnum.ROOM_POLL;
}
public get eventTypes(): string[]
{
return [RoomSessionPollEvent.OFFER, RoomSessionPollEvent.ERROR, RoomSessionPollEvent.CONTENT];
}
public get messageTypes(): string[]
{
return [RoomWidgetPollMessage.ANSWER, RoomWidgetPollMessage.REJECT, RoomWidgetPollMessage.START];
}
}

View File

@ -0,0 +1,74 @@
import { AvatarAction, NitroEvent, RoomSessionWordQuizEvent, RoomWidgetEnum } from '@nitrots/nitro-renderer';
import { RoomWidgetHandler } from '.';
import { GetRoomEngine } from '../../GetRoomEngine';
import { RoomWidgetUpdateEvent } from '../events';
import { RoomWidgetWordQuizUpdateEvent } from '../events/RoomWidgetWordQuizUpdateEvent';
import { RoomWidgetMessage } from '../messages';
export class WordQuizWidgetHandler extends RoomWidgetHandler
{
public processEvent(event: NitroEvent): void
{
const roomQuizEvent = (event as RoomSessionWordQuizEvent);
let widgetEvent: RoomWidgetWordQuizUpdateEvent;
switch(event.type)
{
case RoomSessionWordQuizEvent.ANSWERED:
const roomId = this.container.roomSession.roomId;
const userData = this.container.roomSession.userDataManager.getUserData(roomQuizEvent.userId);
if(!userData) return;
widgetEvent = new RoomWidgetWordQuizUpdateEvent(RoomWidgetWordQuizUpdateEvent.QUESTION_ANSWERED, roomQuizEvent.id);
widgetEvent.value = roomQuizEvent.value;
widgetEvent.userId = roomQuizEvent.userId;
widgetEvent.answerCounts = roomQuizEvent.answerCounts;
if(widgetEvent.value === '0')
{
GetRoomEngine().updateRoomObjectUserGesture(roomId, userData.roomIndex, AvatarAction.getGestureId(AvatarAction.GESTURE_SAD));
}
else
{
GetRoomEngine().updateRoomObjectUserGesture(roomId, userData.roomIndex, AvatarAction.getGestureId(AvatarAction.GESTURE_SMILE));
}
break;
case RoomSessionWordQuizEvent.FINISHED:
widgetEvent = new RoomWidgetWordQuizUpdateEvent(RoomWidgetWordQuizUpdateEvent.QUESTION_FINISHED, roomQuizEvent.id);
widgetEvent.pollId = roomQuizEvent.pollId;
widgetEvent.questionId = roomQuizEvent.questionId;
widgetEvent.answerCounts = roomQuizEvent.answerCounts;
break;
case RoomSessionWordQuizEvent.QUESTION:
widgetEvent = new RoomWidgetWordQuizUpdateEvent(RoomWidgetWordQuizUpdateEvent.NEW_QUESTION, roomQuizEvent.id);
widgetEvent.question = roomQuizEvent.question;
widgetEvent.duration = roomQuizEvent.duration;
widgetEvent.pollType = roomQuizEvent.pollType;
widgetEvent.questionId = roomQuizEvent.questionId;
widgetEvent.pollId = roomQuizEvent.pollId;
break;
}
if(!widgetEvent) return;
this.container.eventDispatcher.dispatchEvent(widgetEvent);
}
public processWidgetMessage(message: RoomWidgetMessage): RoomWidgetUpdateEvent
{
return null;
}
public get type(): string
{
return RoomWidgetEnum.WORD_QUIZZ;
}
public get eventTypes(): string[]
{
return [RoomSessionWordQuizEvent.ANSWERED, RoomSessionWordQuizEvent.FINISHED, RoomSessionWordQuizEvent.QUESTION];
}
public get messageTypes(): string[]
{
return [];
}
}

View File

@ -0,0 +1,44 @@
import { RoomWidgetMessage } from './RoomWidgetMessage';
export class RoomWidgetPollMessage extends RoomWidgetMessage
{
public static readonly START = 'RWPM_START';
public static readonly REJECT = 'RWPM_REJECT';
public static readonly ANSWER = 'RWPM_ANSWER';
private _id = -1;
private _questionId = 0;
private _answers: string[] = null;
constructor(type: string, id: number)
{
super(type);
this._id = id;
}
public get id(): number
{
return this._id;
}
public get questionId(): number
{
return this._questionId;
}
public set questionId(k: number)
{
this._questionId = k;
}
public get answers(): string[]
{
return this._answers;
}
public set answers(k: string[])
{
this._answers = k;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 174 B

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

View File

Before

Width:  |  Height:  |  Size: 169 B

After

Width:  |  Height:  |  Size: 169 B

View File

Before

Width:  |  Height:  |  Size: 199 B

After

Width:  |  Height:  |  Size: 199 B

View File

Before

Width:  |  Height:  |  Size: 173 B

After

Width:  |  Height:  |  Size: 173 B

View File

Before

Width:  |  Height:  |  Size: 162 B

After

Width:  |  Height:  |  Size: 162 B

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 201 B

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

View File

Before

Width:  |  Height:  |  Size: 177 B

After

Width:  |  Height:  |  Size: 177 B

View File

Before

Width:  |  Height:  |  Size: 264 B

After

Width:  |  Height:  |  Size: 264 B

View File

Before

Width:  |  Height:  |  Size: 257 B

After

Width:  |  Height:  |  Size: 257 B

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 205 B

After

Width:  |  Height:  |  Size: 205 B

View File

Before

Width:  |  Height:  |  Size: 225 B

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

View File

@ -159,18 +159,6 @@
}
}
&.icon-deny {
background: url("../images/icons/deny.png");
width: 13px;
height: 14px;
}
&.icon-accept {
background: url("../images/icons/accept.png");
width: 13px;
height: 14px;
}
&.icon-wired-trigger {
background-image: url("../images/wired/icon_trigger.png");
width: 13px;
@ -189,176 +177,6 @@
height: 14px;
}
&.arrow-left-icon {
background-image: url("../images/avatareditor/arrow-left-icon.png");
width: 28px;
height: 21px;
}
&.arrow-right-icon {
background-image: url("../images/avatareditor/arrow-right-icon.png");
width: 28px;
height: 21px;
}
&.clear-icon {
background-image: url("../images/avatareditor/clear-icon.png");
width: 27px;
height: 27px;
}
&.ca-icon {
background-image: url("../images/avatareditor/ca-icon.png");
width: 25px;
height: 25px;
&.selected {
background-image: url("../images/avatareditor/ca-selected-icon.png");
}
}
&.cc-icon {
background-image: url("../images/avatareditor/cc-icon.png");
width: 31px;
height: 29px;
&.selected {
background-image: url("../images/avatareditor/cc-selected-icon.png");
}
}
&.ch-icon {
background-image: url("../images/avatareditor/ch-icon.png");
width: 29px;
height: 24px;
&.selected {
background-image: url("../images/avatareditor/ch-selected-icon.png");
}
}
&.cp-icon {
background-image: url("../images/avatareditor/cp-icon.png");
width: 30px;
height: 24px;
&.selected {
background-image: url("../images/avatareditor/cp-selected-icon.png");
}
}
&.ea-icon {
background-image: url("../images/avatareditor/ea-icon.png");
width: 35px;
height: 16px;
&.selected {
background-image: url("../images/avatareditor/ea-selected-icon.png");
}
}
&.fa-icon {
background-image: url("../images/avatareditor/fa-icon.png");
width: 27px;
height: 20px;
&.selected {
background-image: url("../images/avatareditor/fa-selected-icon.png");
}
}
&.female-icon {
background-image: url("../images/avatareditor/female-icon.png");
width: 18px;
height: 27px;
&.selected {
background-image: url("../images/avatareditor/female-selected-icon.png");
}
}
&.ha-icon {
background-image: url("../images/avatareditor/ha-icon.png");
width: 25px;
height: 22px;
&.selected {
background-image: url("../images/avatareditor/ha-selected-icon.png");
}
}
&.he-icon {
background-image: url("../images/avatareditor/he-icon.png");
width: 31px;
height: 27px;
&.selected {
background-image: url("../images/avatareditor/he-selected-icon.png");
}
}
&.hr-icon {
background-image: url("../images/avatareditor/hr-icon.png");
width: 29px;
height: 25px;
&.selected {
background-image: url("../images/avatareditor/hr-selected-icon.png");
}
}
&.lg-icon {
background-image: url("../images/avatareditor/lg-icon.png");
width: 19px;
height: 20px;
&.selected {
background-image: url("../images/avatareditor/lg-selected-icon.png");
}
}
&.loading-icon {
background-image: url("../images/icons/loading-icon.png");
width: 17px;
height: 21px;
}
&.male-icon {
background-image: url("../images/avatareditor/male-icon.png");
width: 21px;
height: 21px;
&.selected {
background-image: url("../images/avatareditor/male-selected-icon.png");
}
}
&.sh-icon {
background-image: url("../images/avatareditor/sh-icon.png");
width: 37px;
height: 10px;
&.selected {
background-image: url("../images/avatareditor/sh-selected-icon.png");
}
}
&.wa-icon {
background-image: url("../images/avatareditor/wa-icon.png");
width: 36px;
height: 18px;
&.selected {
background-image: url("../images/avatareditor/wa-selected-icon.png");
}
}
&.sellable-icon {
background-image: url("../images/avatareditor/sellable-icon.png");
width: 17px;
height: 15px;
}
&.chatstyles-icon {
background-image: url("../images/chat/styles-icon.png");
width: 17px;
@ -491,12 +309,6 @@
height: 15px;
}
&.icon-fb-profile {
background: url("../images/toolbar/icons/friend-bar/profile.png");
width: 21px;
height: 21px;
}
&.icon-camera-colormatrix {
background: url("../images/icons/camera-colormatrix.png");
width: 32px;
@ -509,34 +321,6 @@
height: 21px;
}
&.icon-user-profile {
background: url("../images/icons/user-profile.png");
width: 13px;
height: 11px;
&:hover {
background: url("../images/icons/user-profile-hover.png");
}
}
&.icon-fb-profile {
background: url("../images/toolbar/icons/friend-bar/profile.png");
width: 21px;
height: 21px;
}
&.icon-fb-chat {
background: url("../images/toolbar/icons/friend-bar/chat.png");
width: 20px;
height: 21px;
}
&.icon-fb-visit {
background: url("../images/toolbar/icons/friend-bar/visit.png");
width: 21px;
height: 21px;
}
&.icon-pf-online {
background: url("../images/profile/icons/online.gif");
width: 40px;
@ -555,30 +339,6 @@
height: 10px;
}
&.icon-relationship-none {
background: url("../images/friendlist/icons/icon_relationship_none.png");
width: 16px;
height: 14px;
}
&.icon-relationship-heart {
background: url("../images/profile/icons/heart.png");
width: 16px;
height: 14px;
}
&.icon-relationship-bobba {
background: url("../images/profile/icons/bobba.png");
width: 16px;
height: 14px;
}
&.icon-relationship-smile {
background: url("../images/profile/icons/smile.png");
width: 16px;
height: 14px;
}
&.icon-group-type-0 {
background: url("../images/groups/icons/grouptype_icon_0.png");
width: 16px;
@ -681,18 +441,6 @@
height: 11px;
}
&.icon-friendlist-follow {
background: url("../images/friendlist/icons/icon_follow.png");
width: 16px;
height: 14px;
}
&.icon-friendlist-chat {
background: url("../images/friendlist/icons/icon_chat.png");
width: 17px;
height: 16px;
}
&.icon-youtube-next {
background: url("../images/room-widgets/youtube-widget/next.png");
width: 21px;
@ -704,17 +452,6 @@
width: 21px;
height: 16px;
}
&.icon-friendlist-warning {
background: url("../images/friendlist/icons/icon_warning.png");
width: 23px;
height: 21px;
}
&.icon-friendlist-new-message {
background: url("../images/friendlist/icons/icon_new_message.png");
width: 14px;
height: 16px;
}
&.icon-hc-banner {
background: url("../images/catalog/hc_big.png");

View File

@ -70,3 +70,11 @@ ul {
.grayscale {
filter: grayscale(1);
}
.flex-none {
flex: none;
}
.z-index-1 {
z-index: 1;
}

View File

@ -3,7 +3,7 @@ import { NitroLayoutBaseProps } from './NitroLayoutBase.types';
export const NitroLayoutBase: FC<NitroLayoutBaseProps> = props =>
{
const { className = '', overflow = null, position = null, gap = null, children = null, ...rest } = props;
const { className = '', overflow = null, position = null, gap = null, ref = null, innerRef = null, children = null, ...rest } = props;
const getClassName = useMemo(() =>
{
@ -21,7 +21,7 @@ export const NitroLayoutBase: FC<NitroLayoutBaseProps> = props =>
}, [ className, overflow, position, gap ]);
return (
<div className={ getClassName } { ...rest }>
<div className={ getClassName } ref={ innerRef } { ...rest }>
{ children }
</div>
);

View File

@ -1,8 +1,9 @@
import { DetailedHTMLProps, HTMLAttributes } from 'react';
import { DetailedHTMLProps, HTMLAttributes, LegacyRef } from 'react';
import { NitroLayoutOverflow, NitroLayoutPosition, NitroLayoutSpacing } from '../common';
export interface NitroLayoutBaseProps extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
{
innerRef?: LegacyRef<HTMLDivElement>;
overflow?: NitroLayoutOverflow;
position?: NitroLayoutPosition;
gap?: NitroLayoutSpacing;

View File

@ -0,0 +1,21 @@
import { createContext, Dispatch, FC, ProviderProps, SetStateAction, useContext } from 'react';
export interface INitroCardAccordionContext
{
closers: Function[];
setClosers: Dispatch<SetStateAction<Function[]>>;
closeAll: () => void;
}
const NitroCardAccordionContext = createContext<INitroCardAccordionContext>({
closers: null,
setClosers: null,
closeAll: null
});
export const NitroCardAccordionContextProvider: FC<ProviderProps<INitroCardAccordionContext>> = props =>
{
return <NitroCardAccordionContext.Provider { ...props } />;
}
export const useNitroCardAccordionContext = () => useContext(NitroCardAccordionContext);

View File

@ -1 +1,6 @@
.nitro-card-accordion {
display: flex;
height: 100%;
}
@import "./set/NitroCardAccordionSetView";

View File

@ -1,13 +1,32 @@
import { FC } from 'react';
import { FC, useCallback, useMemo, useState } from 'react';
import { NitroLayoutFlexColumn } from '../..';
import { NitroCardAccordionContextProvider } from './NitroCardAccordionContext';
import { NitroCardAccordionViewProps } from './NitroCardAccordionView.types';
export const NitroCardAccordionView: FC<NitroCardAccordionViewProps> = props =>
{
const { className = '' } = props;
const { className = '', children = null, ...rest } = props;
const [ closers, setClosers ] = useState<Function[]>([]);
const getClassName = useMemo(() =>
{
let newClassName = 'nitro-card-accordion text-black';
if(className && className.length) newClassName += ` ${ className }`;
return newClassName;
}, [ className ]);
const closeAll = useCallback(() =>
{
for(const closer of closers) closer();
}, [ closers ]);
return (
<div className={ 'nitro-card-accordion text-black ' + className }>
{ props.children }
</div>
<NitroCardAccordionContextProvider value={ { closers, setClosers, closeAll } }>
<NitroLayoutFlexColumn className={ getClassName } { ...rest }>
{ children }
</NitroLayoutFlexColumn>
</NitroCardAccordionContextProvider>
);
}

View File

@ -1,4 +1,6 @@
export interface NitroCardAccordionViewProps
import { NitroLayoutFlexColumnProps } from '../..';
export interface NitroCardAccordionViewProps extends NitroLayoutFlexColumnProps
{
className?: string;
}

View File

@ -1,9 +1,12 @@
.nitro-card-accordion-set {
.nitro-card-accordion-set-header {
&.active {
height: 100%;
background: rgba($white, 0.5);
border-bottom: 1px solid rgba($black, 0.2);
}
.nitro-card-accordion-set-content {
background: rgba($white, 0.5);
.nitro-card-accordion-set-header {
border-bottom: 1px solid rgba($black, 0.2);
}
}

View File

@ -1,13 +1,15 @@
import classNames from 'classnames';
import { FC, useEffect, useMemo, useState } from 'react';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { NitroLayoutFlex } from '../../..';
import { NitroLayoutBase } from '../../../base';
import { useNitroCardAccordionContext } from '../NitroCardAccordionContext';
import { NitroCardAccordionSetViewProps } from './NitroCardAccordionSetView.types';
export const NitroCardAccordionSetView: FC<NitroCardAccordionSetViewProps> = props =>
{
const { headerText = '', isExpanded = false, className = '', children = null, ...rest } = props;
const [ isOpen, setIsOpen ] = useState(false);
const { setClosers = null, closeAll = null } = useNitroCardAccordionContext();
const getClassName = useMemo(() =>
{
@ -20,14 +22,66 @@ export const NitroCardAccordionSetView: FC<NitroCardAccordionSetViewProps> = pro
return newClassName;
}, [ className, isOpen ]);
const onClick = useCallback(() =>
{
closeAll();
setIsOpen(prevValue => !prevValue);
// if(isOpen)
// {
// closeAll();
// }
// else
// {
// BatchUpdates(() =>
// {
// closeAll();
// setIsOpen(true);
// });
// }
}, [ closeAll ]);
const close = useCallback(() =>
{
setIsOpen(false);
}, []);
useEffect(() =>
{
setIsOpen(isExpanded);
}, [ isExpanded ]);
useEffect(() =>
{
const closeFunction = close;
setClosers(prevValue =>
{
const newClosers = [ ...prevValue ];
newClosers.push(closeFunction);
return newClosers;
});
return () =>
{
setClosers(prevValue =>
{
const newClosers = [ ...prevValue ];
const index = newClosers.indexOf(closeFunction);
if(index >= 0) newClosers.splice(index, 1);
return newClosers;
});
}
}, [ close, setClosers ]);
return (
<NitroLayoutBase className={ getClassName } { ...rest }>
<NitroLayoutFlex className="nitro-card-accordion-set-header px-2 py-1" onClick={ event => setIsOpen(!isOpen) }>
<NitroLayoutFlex className="nitro-card-accordion-set-header px-2 py-1" onClick={ onClick }>
<div className="w-100">
{ headerText }
</div>

View File

@ -1 +1 @@
export type NitroLayoutVariant = 'primary' | 'success' | 'danger' | 'secondary';
export type NitroLayoutVariant = 'primary' | 'success' | 'danger' | 'secondary' | 'link' | 'black';

View File

@ -14,3 +14,4 @@ export * from './notification-alert';
export * from './notification-bubble';
export * from './transitions';
export * from './trophy';
export * from './user-profile-icon';

View File

@ -0,0 +1,26 @@
import { FC, useMemo } from 'react';
import { GetUserProfile } from '../../api';
import { NitroLayoutBase } from '../base';
import { UserProfileIconViewProps } from './UserProfileIconView.types';
export const UserProfileIconView: FC<UserProfileIconViewProps> = props =>
{
const { userId = 0, userName = null } = props;
const { className = '', children = null, ...rest } = props;
const getClassName = useMemo(() =>
{
let newClassName = 'nitro-friends-spritesheet icon-profile-sm me-1 cursor-pointer';
if(className && className.length) newClassName += ` ${ className }`;
return newClassName;
}, [ className ]);
return (
<NitroLayoutBase className={ getClassName } onClick={ event => GetUserProfile(userId) }>
{ children }
</NitroLayoutBase>
);
}

View File

@ -0,0 +1,7 @@
import { NitroLayoutBaseProps } from '../base';
export interface UserProfileIconViewProps extends NitroLayoutBaseProps
{
userId?: number;
userName?: string;
}

View File

@ -0,0 +1,2 @@
export * from './UserProfileIconView';
export * from './UserProfileIconView.types';

View File

@ -1,3 +1,217 @@
.nitro-avatar-editor-spritesheet {
background: url('../../assets/images/avatareditor/avatar-editor-spritesheet.png') transparent no-repeat;
&.arrow-left-icon {
width: 28px;
height: 21px;
background-position: -226px -131px;
}
&.arrow-right-icon {
width: 28px;
height: 21px;
background-position: -226px -162px;
}
&.ca-icon {
width: 25px;
height: 25px;
background-position: -226px -61px;
&.selected {
width: 25px;
height: 25px;
background-position: -226px -96px;
}
}
&.cc-icon {
width: 31px;
height: 29px;
background-position: -145px -5px;
&.selected {
width: 31px;
height: 29px;
background-position: -145px -44px;
}
}
&.ch-icon {
width: 29px;
height: 24px;
background-position: -186px -39px;
&.selected {
width: 29px;
height: 24px;
background-position: -186px -73px;
}
}
&.clear-icon {
width: 27px;
height: 27px;
background-position: -145px -157px;
}
&.cp-icon {
width: 30px;
height: 24px;
background-position: -145px -264px;
&.selected {
width: 30px;
height: 24px;
background-position: -186px -5px;
}
}
&.ea-icon {
width: 35px;
height: 16px;
background-position: -226px -193px;
&.selected {
width: 35px;
height: 16px;
background-position: -226px -219px;
}
}
&.fa-icon {
width: 27px;
height: 20px;
background-position: -186px -137px;
&.selected {
width: 27px;
height: 20px;
background-position: -186px -107px;
}
}
&.female-icon {
width: 18px;
height: 27px;
background-position: -186px -202px;
&.selected {
width: 18px;
height: 27px;
background-position: -186px -239px;
}
}
&.ha-icon {
width: 25px;
height: 22px;
background-position: -226px -245px;
&.selected {
width: 25px;
height: 22px;
background-position: -226px -277px;
}
}
&.he-icon {
width: 31px;
height: 27px;
background-position: -145px -83px;
&.selected {
width: 31px;
height: 27px;
background-position: -145px -120px;
}
}
&.hr-icon {
width: 29px;
height: 25px;
background-position: -145px -194px;
&.selected {
width: 29px;
height: 25px;
background-position: -145px -229px;
}
}
&.lg-icon {
width: 19px;
height: 20px;
background-position: -303px -45px;
&.selected {
width: 19px;
height: 20px;
background-position: -303px -75px;
}
}
&.loading-icon {
width: 21px;
height: 25px;
background-position: -186px -167px;
}
&.male-icon {
width: 21px;
height: 21px;
background-position: -186px -276px;
&.selected {
width: 21px;
height: 21px;
background-position: -272px -5px;
}
}
&.sellable-icon {
width: 17px;
height: 15px;
background-position: -303px -105px;
}
&.sh-icon {
width: 37px;
height: 10px;
background-position: -303px -5px;
&.selected {
width: 37px;
height: 10px;
background-position: -303px -25px;
}
}
&.spotlight {
width: 130px;
height: 305px;
background-position: -5px -5px;
}
&.wa-icon {
width: 36px;
height: 18px;
background-position: -226px -5px;
&.selected {
width: 36px;
height: 18px;
background-position: -226px -33px;
}
}
}
.nitro-avatar-editor {
width: $avatar-editor-width;
height: $avatar-editor-height;
@ -37,13 +251,10 @@
z-index: 4;
}
.avatar-spotlight {
.spotlight {
position: absolute;
top: -10px;
width: 100%;
height: 305px;
margin: 0 auto;
background: transparent url('../../assets/images/avatareditor/spotlight.png') no-repeat center;
opacity: 0.3;
pointer-events: none;
z-index: 3;

View File

@ -45,11 +45,11 @@ export const AvatarEditorFigurePreviewView: FC<AvatarEditorFigurePreviewViewProp
return (
<NitroLayoutFlexColumn className="figure-preview-container" overflow="hidden" position="relative">
<AvatarImageView figure={ figureData.getFigureString() } direction={ figureData.direction } scale={ 2 } />
<NitroLayoutBase className="avatar-spotlight" />
<NitroLayoutBase className="nitro-avatar-editor-spritesheet spotlight" />
<NitroLayoutBase className="avatar-shadow" />
<NitroLayoutBase className="arrow-container">
<i className="icon arrow-left-icon" onClick={ event => rotateFigure(figureData.direction + 1) } />
<i className="icon arrow-right-icon" onClick={ event => rotateFigure(figureData.direction - 1) } />
<div className="nitro-avatar-editor-spritesheet arrow-left-icon" onClick={ event => rotateFigure(figureData.direction + 1) } />
<div className="nitro-avatar-editor-spritesheet arrow-right-icon" onClick={ event => rotateFigure(figureData.direction - 1) } />
</NitroLayoutBase>
</NitroLayoutFlexColumn>
);

View File

@ -26,8 +26,8 @@ export const AvatarEditorFigureSetItemView: FC<AvatarEditorFigureSetItemViewProp
return (
<NitroCardGridItemView itemImage={ (partItem.isClear ? undefined : partItem.imageUrl) } itemActive={ partItem.isSelected } onClick={ event => onClick(partItem) }>
{ partItem.isHC && <CurrencyIcon className="position-absolute end-1 bottom-1" type={ 'hc' } /> }
{ partItem.isClear && <i className="icon clear-icon" /> }
{ partItem.isSellable && <i className="position-absolute icon sellable-icon end-1 bottom-1" /> }
{ partItem.isClear && <div className="nitro-avatar-editor-spritesheet clear-icon" /> }
{ partItem.isSellable && <div className="position-absolute nitro-avatar-editor-spritesheet sellable-icon end-1 bottom-1" /> }
</NitroCardGridItemView>
);
}

View File

@ -52,10 +52,10 @@ export const AvatarEditorModelView: FC<AvatarEditorModelViewProps> = props =>
{ model.canSetGender &&
<>
<NitroLayoutFlex className="justify-content-center align-items-center category-item cursor-pointer" onClick={ event => setGender(FigureData.MALE) }>
<i className={ `icon male-icon ${ (gender === FigureData.MALE) ? ' selected' : ''}` } />
<div className={ `nitro-avatar-editor-spritesheet male-icon ${ (gender === FigureData.MALE) ? ' selected' : ''}` } />
</NitroLayoutFlex>
<NitroLayoutFlex className="justify-content-center align-items-center category-item cursor-pointer" onClick={ event => setGender(FigureData.FEMALE) }>
<i className={ `icon female-icon ${ (gender === FigureData.FEMALE) ? ' selected' : ''}` } />
<div className={ `nitro-avatar-editor-spritesheet female-icon ${ (gender === FigureData.FEMALE) ? ' selected' : ''}` } />
</NitroLayoutFlex>
</> }
{ !model.canSetGender && model.categories && (model.categories.size > 0) && Array.from(model.categories.keys()).map(name =>
@ -64,7 +64,7 @@ export const AvatarEditorModelView: FC<AvatarEditorModelViewProps> = props =>
return (
<NitroLayoutFlex key={ name } className="justify-content-center align-items-center category-item cursor-pointer" onClick={ event => selectCategory(name) }>
<i className={ `icon ${ category.name }-icon ${ (activeCategory === category) ? ' selected' : ''}` } />
<div className={ `nitro-avatar-editor-spritesheet ${ category.name }-icon ${ (activeCategory === category) ? ' selected' : ''}` } />
</NitroLayoutFlex>
);
})}

View File

@ -1,18 +1,13 @@
.nitro-chat-history
{
width: 300px;
.nitro-chat-history {
width: $chat-history-width;
height: $chat-history-height;
.chat-history-content
{
.chat-history-container
{
.chat-history-content {
.chat-history-container {
min-height: 200px;
.chat-history-list
{
.chathistory-entry
{
.chat-history-list {
.chathistory-entry {
.light {
background-color: #121f27;
}

View File

@ -1,6 +1,95 @@
.nitro-friends-spritesheet {
background: url('../../assets/images/friends/friends-spritesheet.png') transparent no-repeat;
&.icon-friendbar-visit {
width: 21px; height: 21px;
background-position: -38px -5px;
}
&.icon-heart {
width: 16px; height: 14px;
background-position: -5px -67px;
}
&.icon-new-message {
width: 14px; height: 14px;
background-position: -96px -53px;
}
&.icon-none {
width: 16px; height: 14px;
background-position: -31px -67px;
}
&.icon-profile {
width: 21px; height: 21px;
background-position: -5px -36px;
}
&.icon-profile-sm {
width: 13px; height: 11px;
background-position: -51px -91px;
&:hover {
width: 13px; height: 11px;
background-position: -74px -91px;
}
}
&.icon-smile {
width: 16px; height: 14px;
background-position: -57px -67px;
}
&.icon-warning {
width: 23px; height: 21px;
background-position: -5px -5px;
}
&.icon-accept {
width: 13px; height: 14px;
background-position: -5px -91px;
}
&.icon-add {
width: 16px; height: 15px;
background-position: -69px -31px;
}
&.icon-bobba {
width: 16px; height: 14px;
background-position: -96px -5px;
}
&.icon-chat {
width: 17px; height: 16px;
background-position: -69px -5px;
}
&.icon-deny {
width: 13px; height: 14px;
background-position: -28px -91px;
}
&.icon-follow {
width: 16px; height: 14px;
background-position: -96px -29px;
}
&.icon-friendbar-chat {
width: 20px; height: 21px;
background-position: -36px -36px;
}
}
.nitro-friends {
width: $friends-list-width;
height: $friends-list-height;
.search-input {
border: 0;
border-bottom: 1px solid rgba($black, 0.2);
}
}
@import "./views/friend-bar/FriendBarView";

View File

@ -344,7 +344,7 @@ export const FriendsView: FC<{}> = props =>
}, [ requests ]);
return (
<FriendsContextProvider value={ { friends, requests, settings, acceptFriend, declineFriend } }>
<FriendsContextProvider value={ { friends, requests, settings, canRequestFriend, requestFriend, acceptFriend, declineFriend } }>
{ isReady &&
createPortal(<FriendBarView onlineFriends={ onlineFriends } />, document.getElementById('toolbar-friend-bar-container')) }
{ isVisible &&

View File

@ -5,6 +5,8 @@ const FriendsContext = createContext<IFriendsContext>({
friends: null,
requests: null,
settings: null,
canRequestFriend: null,
requestFriend: null,
acceptFriend: null,
declineFriend: null
});

View File

@ -8,6 +8,8 @@ export interface IFriendsContext
friends: MessengerFriend[];
requests: MessengerRequest[];
settings: MessengerSettings;
canRequestFriend: (userId: number) => boolean;
requestFriend: (userId: number, userName: string) => void;
acceptFriend: (userId: number) => void;
declineFriend: (userId: number, declineAll?: boolean) => void;
}

View File

@ -2,6 +2,7 @@ import { FollowFriendMessageComposer, MouseEventType } from '@nitrots/nitro-rend
import { FC, useCallback, useEffect, useRef, useState } from 'react';
import { GetUserProfile, LocalizeText, OpenMessengerChat } from '../../../../api';
import { SendMessageHook } from '../../../../hooks/messages';
import { NitroLayoutBase } from '../../../../layout/base';
import { AvatarImageView } from '../../../shared/avatar-image/AvatarImageView';
import { FriendBarItemViewProps } from './FriendBarItemView.types';
@ -63,9 +64,10 @@ export const FriendBarItemView: FC<FriendBarItemViewProps> = props =>
<div className="text-truncate">{ friend.name }</div>
{ isVisible &&
<div className="d-flex justify-content-between">
<i onClick={ openMessengerChat } className="icon icon-fb-chat cursor-pointer" />
{ friend.followingAllowed && <i onClick={ followFriend } className="icon icon-fb-visit cursor-pointer" /> }
<i onClick={ event => GetUserProfile(friend.id) } className="icon icon-fb-profile cursor-pointer" />
<NitroLayoutBase className="nitro-friends-spritesheet icon-friendbar-chat cursor-pointer" onClick={ openMessengerChat } />
{ friend.followingAllowed &&
<NitroLayoutBase className="nitro-friends-spritesheet icon-friendbar-visit cursor-pointer" onClick={ followFriend } /> }
<NitroLayoutBase className="nitro-friends-spritesheet icon-profile cursor-pointer" onClick={ event => GetUserProfile(friend.id) } />
</div> }
</div>
);

View File

@ -1,4 +1,6 @@
import { FC, useMemo, useState } from 'react';
import { NitroLayoutButton, NitroLayoutFlex } from '../../../../layout';
import { NitroLayoutBase } from '../../../../layout/base';
import { FriendBarItemView } from '../friend-bar-item/FriendBarItemView';
import { FriendBarViewProps } from './FriendBarView.types';
@ -24,10 +26,10 @@ export const FriendBarView: FC<FriendBarViewProps> = props =>
}, [ maxDisplayCount, indexOffset, onlineFriends ]);
return (
<div className="d-flex friend-bar align-items-center">
<button type="button" className="btn btn-sm btn-black align-self-center friend-bar-button" disabled={ !canDecreaseIndex } onClick={ event => setIndexOffset(indexOffset - 1) }>
<i className="fas fa-chevron-left" />
</button>
<NitroLayoutFlex className="friend-bar align-items-center">
<NitroLayoutButton className="friend-bar-button" variant="black" size="sm" disabled={ !canDecreaseIndex } onClick={ event => setIndexOffset(indexOffset - 1) }>
<NitroLayoutBase className="fas fa-chevron-left" />
</NitroLayoutButton>
{ Array.from(Array(maxDisplayCount), (e, i) =>
{
return <FriendBarItemView key={ i } friend={ (onlineFriends[ indexOffset + i ] || null) } />;
@ -35,6 +37,6 @@ export const FriendBarView: FC<FriendBarViewProps> = props =>
<button type="button" className="btn btn-sm btn-black align-self-center friend-bar-button" disabled={ !canIncreaseIndex } onClick={ event => setIndexOffset(indexOffset + 1) }>
<i className="fas fa-chevron-right" />
</button>
</div>
</NitroLayoutFlex>
);
}

View File

@ -2,7 +2,8 @@ import { FollowFriendMessageComposer, SetRelationshipStatusComposer } from '@nit
import { FC, useCallback, useState } from 'react';
import { LocalizeText, OpenMessengerChat } from '../../../../api';
import { SendMessageHook } from '../../../../hooks';
import { UserProfileIconView } from '../../../shared/user-profile-icon/UserProfileIconView';
import { NitroLayoutFlex, UserProfileIconView } from '../../../../layout';
import { NitroLayoutBase } from '../../../../layout/base';
import { MessengerFriend } from '../../common/MessengerFriend';
import { FriendsGroupItemViewProps } from './FriendsGroupItemView.types';
@ -52,19 +53,23 @@ export const FriendsGroupItemView: FC<FriendsGroupItemViewProps> = props =>
<div className="px-2 py-1 d-flex gap-1 align-items-center">
<UserProfileIconView userId={ friend.id } />
<div>{ friend.name }</div>
<div className="ms-auto d-flex align-items-center gap-1">
{ !isExpanded && <>
{ friend.followingAllowed && <i onClick={ followFriend } className="icon icon-friendlist-follow cursor-pointer" title={ LocalizeText('friendlist.tip.follow') } /> }
{ friend.online && <i className="icon icon-friendlist-chat cursor-pointer" onClick={ openMessengerChat } title={ LocalizeText('friendlist.tip.im') } /> }
<i className={ 'icon cursor-pointer icon-relationship-' + getCurrentRelationshipName() } onClick={ () => setIsExpanded(true) } title={ LocalizeText('infostand.link.relationship') } />
<NitroLayoutFlex className="ms-auto align-items-center" gap={ 1 }>
{ !isExpanded &&
<>
{ friend.followingAllowed &&
<NitroLayoutBase onClick={ followFriend } className="nitro-friends-spritesheet icon-follow cursor-pointer" title={ LocalizeText('friendlist.tip.follow') } /> }
{ friend.online &&
<NitroLayoutBase className="nitro-friends-spritesheet icon-chat cursor-pointer" onClick={ openMessengerChat } title={ LocalizeText('friendlist.tip.im') } /> }
<NitroLayoutBase className={ `nitro-friends-spritesheet icon-${ getCurrentRelationshipName() } cursor-pointer` }onClick={ event => setIsExpanded(true) } title={ LocalizeText('infostand.link.relationship') } />
</> }
{ isExpanded && <>
<i className="icon icon-relationship-heart cursor-pointer" onClick={ () => updateRelationship(MessengerFriend.RELATIONSHIP_HEART) } />
<i className="icon icon-relationship-smile cursor-pointer" onClick={ () => updateRelationship(MessengerFriend.RELATIONSHIP_SMILE) } />
<i className="icon icon-relationship-bobba cursor-pointer" onClick={ () => updateRelationship(MessengerFriend.RELATIONSHIP_BOBBA) } />
<i className="icon icon-relationship-none cursor-pointer" onClick={ () => updateRelationship(MessengerFriend.RELATIONSHIP_NONE) } />
</> }
</div>
{ isExpanded &&
<>
<NitroLayoutBase className="nitro-friends-spritesheet icon-heart cursor-pointer" onClick={ () => updateRelationship(MessengerFriend.RELATIONSHIP_HEART) } />
<NitroLayoutBase className="nitro-friends-spritesheet icon-smile cursor-pointer" onClick={ () => updateRelationship(MessengerFriend.RELATIONSHIP_SMILE) } />
<NitroLayoutBase className="nitro-friends-spritesheet icon-bobba cursor-pointer" onClick={ () => updateRelationship(MessengerFriend.RELATIONSHIP_BOBBA) } />
<NitroLayoutBase className="nitro-friends-spritesheet icon-none cursor-pointer" onClick={ () => updateRelationship(MessengerFriend.RELATIONSHIP_NONE) } />
</> }
</NitroLayoutFlex>
</div>
);
}

View File

@ -3,6 +3,7 @@ import { LocalizeText } from '../../../../api';
import { NitroCardAccordionSetView, NitroCardAccordionView, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../layout';
import { FriendsGroupView } from '../friends-group/FriendsGroupView';
import { FriendsRequestView } from '../friends-request/FriendsRequestView';
import { FriendsSearchView } from '../friends-search/FriendsSearchView';
import { FriendsListViewProps } from './FriendsListView.types';
const MODE_FRIENDS: number = 0;
@ -15,7 +16,7 @@ export const FriendsListView: FC<FriendsListViewProps> = props =>
const [ mode, setMode ] = useState<number>(0);
return (
<NitroCardView className="nitro-friends">
<NitroCardView className="nitro-friends" uniqueKey="nitro-friends">
<NitroCardHeaderView headerText={ LocalizeText('friendlist.friends') } onCloseClick={ onCloseClick } />
<NitroCardTabsView>
<NitroCardTabsItemView isActive={ (mode === MODE_FRIENDS) } count={ friendRequests.length } onClick={ event => setMode(MODE_FRIENDS) }>
@ -34,13 +35,10 @@ export const FriendsListView: FC<FriendsListViewProps> = props =>
<NitroCardAccordionSetView headerText={ LocalizeText('friendlist.friends.offlinecaption') + ` (${offlineFriends.length})` }>
<FriendsGroupView list={ offlineFriends } />
</NitroCardAccordionSetView>
<FriendsRequestView requests={ friendRequests } />
</NitroCardAccordionView> }
{ (mode === MODE_SEARCH) &&
<>
</> }
<FriendsSearchView /> }
</NitroCardContentView>
</NitroCardView>
);

View File

@ -1,6 +1,6 @@
import { FC } from 'react';
import { NitroCardAccordionItemView } from '../../../../layout';
import { UserProfileIconView } from '../../../shared/user-profile-icon/UserProfileIconView';
import { NitroCardAccordionItemView, NitroLayoutFlex, UserProfileIconView } from '../../../../layout';
import { NitroLayoutBase } from '../../../../layout/base';
import { useFriendsContext } from '../../context/FriendsContext';
import { FriendsRequestItemViewProps } from './FriendsRequestItemView.types';
@ -14,11 +14,11 @@ export const FriendsRequestItemView: FC<FriendsRequestItemViewProps> = props =>
return (
<NitroCardAccordionItemView>
<UserProfileIconView userId={ request.id } />
<div>{ request.name }</div>
<div className="ms-auto d-flex align-items-center gap-1">
<i className="icon icon-accept cursor-pointer" onClick={ event => acceptFriend(request.requesterUserId) } />
<i className="icon icon-deny cursor-pointer" onClick={ event => declineFriend(request.requesterUserId) } />
</div>
<NitroLayoutBase>{ request.name }</NitroLayoutBase>
<NitroLayoutFlex className="ms-auto align-items-center" gap={ 1 }>
<NitroLayoutBase className="nitro-friends-spritesheet icon-accept cursor-pointer" onClick={ event => acceptFriend(request.requesterUserId) } />
<NitroLayoutBase className="nitro-friends-spritesheet icon-deny cursor-pointer" onClick={ event => declineFriend(request.requesterUserId) } />
</NitroLayoutFlex>
</NitroCardAccordionItemView>
);
};

View File

@ -0,0 +1,80 @@
import { HabboSearchComposer, HabboSearchResultData, HabboSearchResultEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { LocalizeText, OpenMessengerChat } from '../../../../api';
import { BatchUpdates, CreateMessageHook, SendMessageHook } from '../../../../hooks';
import { NitroCardAccordionItemView, NitroCardAccordionSetView, NitroCardAccordionView, NitroLayoutFlex, UserProfileIconView } from '../../../../layout';
import { NitroLayoutBase } from '../../../../layout/base';
import { useFriendsContext } from '../../context/FriendsContext';
export const FriendsSearchView: FC<{}> = props =>
{
const [ searchValue, setSearchValue ] = useState('');
const [ friendResults, setFriendResults ] = useState<HabboSearchResultData[]>([]);
const [ otherResults, setOtherResults ] = useState<HabboSearchResultData[]>([]);
const { canRequestFriend = null, requestFriend = null } = useFriendsContext();
const onHabboSearchResultEvent = useCallback((event: HabboSearchResultEvent) =>
{
const parser = event.getParser();
BatchUpdates(() =>
{
setFriendResults(parser.friends);
setOtherResults(parser.others);
});
}, []);
CreateMessageHook(HabboSearchResultEvent, onHabboSearchResultEvent);
useEffect(() =>
{
if(!searchValue || !searchValue.length) return;
const timeout = setTimeout(() =>
{
if(!searchValue || !searchValue.length) return;
SendMessageHook(new HabboSearchComposer(searchValue));
}, 500);
return () => clearTimeout(timeout);
}, [ searchValue ]);
return (
<>
<input type="text" className="search-input form-control form-control-sm w-100 rounded-0" placeholder={ LocalizeText('generic.search') } value={ searchValue } onChange={ event => setSearchValue(event.target.value) } />
<NitroCardAccordionView>
<NitroCardAccordionSetView headerText={ LocalizeText('friendlist.search.friendscaption', [ 'cnt' ], [ friendResults.length.toString() ]) } isExpanded={ true }>
{ (friendResults.length > 0) && friendResults.map(result =>
{
return (
<NitroCardAccordionItemView key={ result.avatarId }>
<UserProfileIconView userId={ result.avatarId } />
<NitroLayoutBase>{ result.avatarName }</NitroLayoutBase>
<NitroLayoutFlex className="ms-auto align-items-center" gap={ 1 }>
{ result.isAvatarOnline &&
<NitroLayoutBase className="nitro-friends-spritesheet icon-chat cursor-pointer" onClick={ event => OpenMessengerChat(result.avatarId) } title={ LocalizeText('friendlist.tip.im') } /> }
</NitroLayoutFlex>
</NitroCardAccordionItemView>
);
}) }
</NitroCardAccordionSetView>
<NitroCardAccordionSetView headerText={ LocalizeText('friendlist.search.otherscaption', [ 'cnt' ], [ otherResults.length.toString() ]) } isExpanded={ true }>
{ (otherResults.length > 0) && otherResults.map(result =>
{
return (
<NitroCardAccordionItemView key={ result.avatarId }>
<UserProfileIconView userId={ result.avatarId } />
<NitroLayoutBase>{ result.avatarName }</NitroLayoutBase>
<NitroLayoutFlex className="ms-auto align-items-center" gap={ 1 }>
{ canRequestFriend(result.avatarId) &&
<NitroLayoutBase className="nitro-friends-spritesheet icon-add cursor-pointer" onClick={ event => requestFriend(result.avatarId, result.avatarName) } title={ LocalizeText('friendlist.tip.addfriend') } /> }
</NitroLayoutFlex>
</NitroCardAccordionItemView>
);
}) }
</NitroCardAccordionSetView>
</NitroCardAccordionView>
</>
);
}

View File

@ -1,5 +1,7 @@
import { FC } from 'react';
import { GetSessionDataManager } from '../../../../api';
import { NitroLayoutFlex } from '../../../../layout';
import { NitroLayoutBase } from '../../../../layout/base';
import { AvatarImageView } from '../../../shared/avatar-image/AvatarImageView';
import { MessengerThreadChat } from '../../common/MessengerThreadChat';
import { FriendsMessengerThreadGroupProps } from './FriendsMessengerThreadGroup.types';
@ -17,13 +19,13 @@ export const FriendsMessengerThreadGroup: FC<FriendsMessengerThreadGroupProps> =
{ group.chats.map((chat, index) =>
{
return (
<div key={ index } className="text-break">
<NitroLayoutBase key={ index } className="text-break">
{ chat.type === MessengerThreadChat.SECURITY_NOTIFICATION &&
<div className="bg-light rounded mb-2 d-flex gap-2 px-2 py-1 small text-muted align-items-center">
<i className="icon icon-friendlist-warning flex-shrink-0" />
<div>{ chat.message }</div>
</div> }
</div>
<NitroLayoutBase className="bg-light rounded mb-2 d-flex gap-2 px-2 py-1 small text-muted align-items-center">
<NitroLayoutBase className="nitro-friends-spritesheet icon-warning flex-shrink-0" />
<NitroLayoutBase>{ chat.message }</NitroLayoutBase>
</NitroLayoutBase> }
</NitroLayoutBase>
);
}) }
</div>
@ -31,18 +33,18 @@ export const FriendsMessengerThreadGroup: FC<FriendsMessengerThreadGroupProps> =
}
return (
<div className={ 'd-flex gap-2 w-100 justify-content-' + (group.userId === 0 ? 'end' : 'start') }>
<NitroLayoutFlex className={ 'w-100 justify-content-' + (group.userId === 0 ? 'end' : 'start') } gap={ 2 }>
{ (group.userId > 0) &&
<div className="message-avatar flex-shrink-0">
<NitroLayoutBase className="message-avatar flex-shrink-0">
<AvatarImageView figure={ thread.participant.figure } direction={ 2 } />
</div> }
<div className={ 'bg-light text-black border-radius mb-2 rounded py-1 px-2 messages-group-' + (group.userId === 0 ? 'right' : 'left') }>
{ group.chats.map((chat, index) => <div key={ index } className="text-break">{ chat.message }</div>) }
</div>
</NitroLayoutBase> }
<NitroLayoutBase className={ 'bg-light text-black border-radius mb-2 rounded py-1 px-2 messages-group-' + (group.userId === 0 ? 'right' : 'left') }>
{ group.chats.map((chat, index) => <NitroLayoutBase key={ index } className="text-break">{ chat.message }</NitroLayoutBase>) }
</NitroLayoutBase>
{ (group.userId === 0) &&
<div className="message-avatar flex-shrink-0">
<NitroLayoutBase className="message-avatar flex-shrink-0">
<AvatarImageView figure={ GetSessionDataManager().figure } direction={ 4 } />
</div> }
</div>
</NitroLayoutBase> }
</NitroLayoutFlex>
);
}

View File

@ -3,7 +3,8 @@ import { FC, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState }
import { AddEventLinkTracker, GetUserProfile, LocalizeText, RemoveLinkEventTracker } from '../../../../api';
import { FriendsMessengerIconEvent } from '../../../../events';
import { BatchUpdates, CreateMessageHook, dispatchUiEvent, SendMessageHook } from '../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView, NitroLayoutFlex } from '../../../../layout';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView, NitroLayoutButton, NitroLayoutButtonGroup, NitroLayoutFlex, NitroLayoutFlexColumn } from '../../../../layout';
import { NitroLayoutBase } from '../../../../layout/base';
import { AvatarImageView } from '../../../shared/avatar-image/AvatarImageView';
import { MessengerThread } from '../../common/MessengerThread';
import { MessengerThreadChat } from '../../common/MessengerThreadChat';
@ -232,7 +233,7 @@ export const FriendsMessengerView: FC<{}> = props =>
if(!isVisible) return null;
return (
<NitroCardView className="nitro-friends-messenger" simple={ true }>
<NitroCardView className="nitro-friends-messenger" uniqueKey="nitro-friends-messenger" simple={ true }>
<NitroCardHeaderView headerText={ LocalizeText('messenger.window.title', [ 'OPEN_CHAT_COUNT' ], [ visibleThreads.length.toString() ]) } onCloseClick={ event => setIsVisible(false) } />
<NitroCardContentView>
<NitroLayoutFlex gap={ 2 } overflow="auto">
@ -241,8 +242,9 @@ export const FriendsMessengerView: FC<{}> = props =>
const messageThreadIndex = messageThreads.indexOf(thread);
return (
<div key={ index } className="friend-head rounded flex-shrink-0 cursor-pointer bg-muted" onClick={ event => setActiveThreadIndex(messageThreadIndex) }>
{ thread.unread && <i className="icon icon-friendlist-new-message" /> }
<div key={ index } className="position-relative friend-head rounded flex-shrink-0 cursor-pointer bg-muted" onClick={ event => setActiveThreadIndex(messageThreadIndex) }>
{ thread.unread &&
<NitroLayoutBase className="position-absolute nitro-friends-spritesheet icon-new-message top-1 end-1 z-index-1" /> }
<AvatarImageView figure={ thread.participant.figure } headOnly={ true } direction={ 3 } />
</div>
);
@ -250,32 +252,38 @@ export const FriendsMessengerView: FC<{}> = props =>
</NitroLayoutFlex>
<NitroLayoutFlex className="align-items-center my-1" position="relative">
{ (activeThreadIndex >= 0) &&
<div className="text-black bg-light pe-2 flex-none">
<NitroLayoutBase className="text-black bg-light pe-2 flex-none">
{ LocalizeText('messenger.window.separator', [ 'FRIEND_NAME' ], [ messageThreads[activeThreadIndex].participant.name ]) }
</div> }
</NitroLayoutBase> }
<hr className="bg-dark m-0 w-100" />
</NitroLayoutFlex>
{ (activeThreadIndex >= 0) &&
<>
<div className="d-flex gap-2 mb-2">
<div className="btn-group">
<button className="d-flex justify-content-center align-items-center btn btn-sm btn-primary" onClick={ followFriend }>
<i className="icon icon-friendlist-follow" />
</button>
<button className="d-flex justify-content-center align-items-center btn btn-sm btn-primary" onClick={ openProfile }>
<i className="icon icon-user-profile" />
</button>
</div>
<button className="btn btn-sm btn-danger">{ LocalizeText('messenger.window.button.report') }</button>
<button className="btn btn-sm btn-primary ms-auto" onClick={ event => closeThread(activeThreadIndex) }><i className="fas fa-times" /></button>
</div>
<div ref={ messagesBox } className="bg-muted p-2 rounded chat-messages mb-2 d-flex flex-column">
<NitroLayoutFlex className="mb-2" gap={ 2 }>
<NitroLayoutButtonGroup>
<NitroLayoutButton variant="primary" size="sm" onClick={ followFriend }>
<NitroLayoutBase className="nitro-friends-spritesheet icon-follow" />
</NitroLayoutButton>
<NitroLayoutButton variant="primary" size="sm" onClick={ openProfile }>
<NitroLayoutBase className="nitro-friends-spritesheet icon-profile-sm" />
</NitroLayoutButton>
</NitroLayoutButtonGroup>
<NitroLayoutButton variant="danger" size="sm" onClick={ openProfile }>
{ LocalizeText('messenger.window.button.report') }
</NitroLayoutButton>
<NitroLayoutButton className="ms-auto" variant="primary" size="sm" onClick={ event => closeThread(activeThreadIndex) }>
<NitroLayoutBase className="fas fa-times" />
</NitroLayoutButton>
</NitroLayoutFlex>
<NitroLayoutFlexColumn innerRef={ messagesBox } className="bg-muted p-2 rounded chat-messages mb-2">
<FriendsMessengerThreadView thread={ messageThreads[activeThreadIndex] } />
</div>
<div className="d-flex gap-2">
</NitroLayoutFlexColumn>
<NitroLayoutFlex gap={ 2 }>
<input type="text" className="form-control form-control-sm" placeholder={ LocalizeText('messenger.window.input.default', [ 'FRIEND_NAME' ], [ messageThreads[activeThreadIndex].participant.name ]) } value={ messageText } onChange={ event => setMessageText(event.target.value) } onKeyDown={ onKeyDown } />
<button className="btn btn-sm btn-success" onClick={ sendMessage }>{ LocalizeText('widgets.chatinput.say') }</button>
</div>
<NitroLayoutButton variant="success" size="sm" onClick={ sendMessage }>
{ LocalizeText('widgets.chatinput.say') }
</NitroLayoutButton>
</NitroLayoutFlex>
</> }
</NitroCardContentView>
</NitroCardView>

View File

@ -1,7 +1,7 @@
import { AcceptFriendMessageComposer, DeclineFriendMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback } from 'react';
import { SendMessageHook } from '../../../../hooks/messages/message-event';
import { UserProfileIconView } from '../../../shared/user-profile-icon/UserProfileIconView';
import { SendMessageHook } from '../../../../hooks';
import { UserProfileIconView } from '../../../../layout';
import { FriendsRequestItemViewProps } from './FriendsRequestItemView.types';
export const FriendsRequestItemView: FC<FriendsRequestItemViewProps> = props =>

View File

@ -1,7 +1,7 @@
import { FC } from 'react';
import { LocalizeText } from '../../../../../api';
import { UserProfileIconView } from '../../../../../layout';
import { AvatarImageView } from '../../../../shared/avatar-image/AvatarImageView';
import { UserProfileIconView } from '../../../../shared/user-profile-icon/UserProfileIconView';
import { HallOfFameItemViewProps } from './HallOfFameItemView.types';
export const HallOfFameItemView: FC<HallOfFameItemViewProps> = props =>

View File

@ -7,9 +7,8 @@ import { FloorplanEditorEvent } from '../../../../events/floorplan-editor/Floorp
import { RoomWidgetThumbnailEvent } from '../../../../events/room-widgets/thumbnail';
import { dispatchUiEvent } from '../../../../hooks/events';
import { SendMessageHook } from '../../../../hooks/messages';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView, UserProfileIconView } from '../../../../layout';
import { BadgeImageView } from '../../../shared/badge-image/BadgeImageView';
import { UserProfileIconView } from '../../../shared/user-profile-icon/UserProfileIconView';
import { useNavigatorContext } from '../../context/NavigatorContext';
import { NavigatorActions } from '../../reducers/NavigatorReducer';
import { NavigatorRoomInfoViewProps } from './NavigatorRoomInfoView.types';

View File

@ -2,7 +2,7 @@ import { RemoveAllRightsMessageComposer, RoomGiveRightsComposer, RoomTakeRightsC
import { FC, useCallback, useMemo } from 'react';
import { LocalizeText } from '../../../../../../api';
import { SendMessageHook } from '../../../../../../hooks';
import { UserProfileIconView } from '../../../../../shared/user-profile-icon/UserProfileIconView';
import { UserProfileIconView } from '../../../../../../layout';
import { NavigatorRoomSettingsRightsTabViewProps } from './NavigatorRoomSettingsRightsTabView.types';
export const NavigatorRoomSettingsRightsTabView: FC<NavigatorRoomSettingsRightsTabViewProps> = props =>

View File

@ -2,6 +2,8 @@ import { EventDispatcher, NitroRectangle, RoomGeometry, RoomVariableEnum, Vector
import { FC, useEffect, useRef, useState } from 'react';
import { DispatchMouseEvent, DispatchTouchEvent, DoorbellWidgetHandler, FriendRequestHandler, FurniChooserWidgetHandler, FurnitureContextMenuWidgetHandler, FurnitureCreditWidgetHandler, FurnitureCustomStackHeightWidgetHandler, FurnitureDimmerWidgetHandler, FurnitureExternalImageWidgetHandler, FurnitureMannequinWidgetHandler, FurniturePresentWidgetHandler, GetNitroInstance, GetRoomEngine, InitializeRoomInstanceRenderingCanvas, IRoomWidgetHandlerManager, RoomWidgetAvatarInfoHandler, RoomWidgetChatHandler, RoomWidgetChatInputHandler, RoomWidgetHandlerManager, RoomWidgetInfostandHandler, RoomWidgetRoomToolsHandler, RoomWidgetUpdateRoomViewEvent, UserChooserWidgetHandler } from '../../api';
import { FurnitureYoutubeDisplayWidgetHandler } from '../../api/nitro/room/widgets/handlers/FurnitureYoutubeDisplayWidgetHandler';
import { PollWidgetHandler } from '../../api/nitro/room/widgets/handlers/PollWidgetHandler';
import { WordQuizWidgetHandler } from '../../api/nitro/room/widgets/handlers/WordQuizWidgetHandler';
import { RoomContextProvider } from './context/RoomContext';
import { RoomColorView } from './RoomColorView';
import { RoomViewProps } from './RoomView.types';
@ -37,6 +39,8 @@ export const RoomView: FC<RoomViewProps> = props =>
widgetHandlerManager.registerHandler(new RoomWidgetChatHandler());
widgetHandlerManager.registerHandler(new UserChooserWidgetHandler());
widgetHandlerManager.registerHandler(new DoorbellWidgetHandler());
widgetHandlerManager.registerHandler(new WordQuizWidgetHandler());
widgetHandlerManager.registerHandler(new PollWidgetHandler());
widgetHandlerManager.registerHandler(new FriendRequestHandler());
widgetHandlerManager.registerHandler(new FurniChooserWidgetHandler());

View File

@ -8,3 +8,4 @@
@import './object-location/ObjectLocationView';
@import './room-tools/RoomToolsWidgetView';
@import './choosers/ChooserWidgetView';
@import './word-quiz/WordQuizWidgetView';

View File

@ -1,4 +1,4 @@
import { RoomEngineEvent, RoomEngineObjectEvent, RoomEngineRoomAdEvent, RoomEngineTriggerWidgetEvent, RoomEngineUseProductEvent, RoomId, RoomObjectCategory, RoomObjectOperationType, RoomObjectVariable, RoomSessionChatEvent, RoomSessionDanceEvent, RoomSessionDimmerPresetsEvent, RoomSessionDoorbellEvent, RoomSessionErrorMessageEvent, RoomSessionEvent, RoomSessionFriendRequestEvent, RoomSessionPetInfoUpdateEvent, RoomSessionPresentEvent, RoomSessionUserBadgesEvent, RoomZoomEvent } from '@nitrots/nitro-renderer';
import { RoomEngineEvent, RoomEngineObjectEvent, RoomEngineRoomAdEvent, RoomEngineTriggerWidgetEvent, RoomEngineUseProductEvent, RoomId, RoomObjectCategory, RoomObjectOperationType, RoomObjectVariable, RoomSessionChatEvent, RoomSessionDanceEvent, RoomSessionDimmerPresetsEvent, RoomSessionDoorbellEvent, RoomSessionErrorMessageEvent, RoomSessionEvent, RoomSessionFriendRequestEvent, RoomSessionPetInfoUpdateEvent, RoomSessionPollEvent, RoomSessionPresentEvent, RoomSessionUserBadgesEvent, RoomSessionWordQuizEvent, RoomZoomEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback } from 'react';
import { CanManipulateFurniture, GetRoomEngine, GetSessionDataManager, IsFurnitureSelectionDisabled, LocalizeText, ProcessRoomObjectOperation, RoomWidgetFurniToWidgetMessage, RoomWidgetUpdateRoomEngineEvent, RoomWidgetUpdateRoomObjectEvent } from '../../../api';
import { useRoomEngineEvent, useRoomSessionManagerEvent } from '../../../hooks/events';
@ -15,6 +15,7 @@ import { FurnitureWidgetsView } from './furniture/FurnitureWidgetsView';
import { InfoStandWidgetView } from './infostand/InfoStandWidgetView';
import { RoomThumbnailWidgetView } from './room-thumbnail/RoomThumbnailWidgetView';
import { RoomToolsWidgetView } from './room-tools/RoomToolsWidgetView';
import { WordQuizWidgetView } from './word-quiz/WordQuizWidgetView';
export const RoomWidgetsView: FC<{}> = props =>
{
@ -268,6 +269,12 @@ export const RoomWidgetsView: FC<{}> = props =>
useRoomSessionManagerEvent(RoomSessionFriendRequestEvent.RSFRE_FRIEND_REQUEST, onRoomSessionEvent);
useRoomSessionManagerEvent(RoomSessionPresentEvent.RSPE_PRESENT_OPENED, onRoomSessionEvent);
useRoomSessionManagerEvent(RoomSessionPetInfoUpdateEvent.PET_INFO, onRoomSessionEvent);
useRoomSessionManagerEvent(RoomSessionWordQuizEvent.ANSWERED, onRoomSessionEvent);
useRoomSessionManagerEvent(RoomSessionWordQuizEvent.FINISHED, onRoomSessionEvent);
useRoomSessionManagerEvent(RoomSessionWordQuizEvent.QUESTION, onRoomSessionEvent);
useRoomSessionManagerEvent(RoomSessionPollEvent.OFFER, onRoomSessionEvent);
useRoomSessionManagerEvent(RoomSessionPollEvent.ERROR, onRoomSessionEvent);
useRoomSessionManagerEvent(RoomSessionPollEvent.CONTENT, onRoomSessionEvent);
const onRoomSessionErrorMessageEvent = useCallback((event: RoomSessionErrorMessageEvent) =>
{
@ -349,6 +356,7 @@ export const RoomWidgetsView: FC<{}> = props =>
<RoomThumbnailWidgetView />
<FurniChooserWidgetView />
<UserChooserWidgetView />
<WordQuizWidgetView />
</>
);
}

View File

@ -2,10 +2,10 @@ import { CrackableDataType, GroupInformationComposer, GroupInformationEvent, Roo
import { FC, useCallback, useEffect, useState } from 'react';
import { CreateLinkEvent, GetGroupInformation, GetRoomEngine, LocalizeText, RoomWidgetFurniActionMessage } from '../../../../../../api';
import { CreateMessageHook, SendMessageHook } from '../../../../../../hooks';
import { UserProfileIconView } from '../../../../../../layout';
import { BadgeImageView } from '../../../../../shared/badge-image/BadgeImageView';
import { LimitedEditionCompactPlateView } from '../../../../../shared/limited-edition/compact-plate/LimitedEditionCompactPlateView';
import { RarityLevelView } from '../../../../../shared/rarity-level/RarityLevelView';
import { UserProfileIconView } from '../../../../../shared/user-profile-icon/UserProfileIconView';
import { useRoomContext } from '../../../../context/RoomContext';
import { InfoStandBaseView } from '../base/InfoStandBaseView';
import { InfoStandWidgetFurniViewProps } from './InfoStandWidgetFurniView.types';

View File

@ -1,7 +1,7 @@
import { FC } from 'react';
import { LocalizeText } from '../../../../../../api';
import { UserProfileIconView } from '../../../../../../layout';
import { PetImageView } from '../../../../../shared/pet-image/PetImageView';
import { UserProfileIconView } from '../../../../../shared/user-profile-icon/UserProfileIconView';
import { InfoStandBaseView } from '../base/InfoStandBaseView';
import { InfoStandWidgetPetViewProps } from './InfoStandWidgetPetView.types';

View File

@ -1,10 +1,10 @@
import { BotRemoveComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useMemo } from 'react';
import { LocalizeText } from '../../../../../../api';
import { SendMessageHook } from '../../../../../../hooks/messages';
import { SendMessageHook } from '../../../../../../hooks';
import { UserProfileIconView } from '../../../../../../layout';
import { AvatarImageView } from '../../../../../shared/avatar-image/AvatarImageView';
import { BadgeImageView } from '../../../../../shared/badge-image/BadgeImageView';
import { UserProfileIconView } from '../../../../../shared/user-profile-icon/UserProfileIconView';
import { BotSkillsEnum } from '../../../avatar-info/common/BotSkillsEnum';
import { InfoStandBaseView } from '../base/InfoStandBaseView';
import { InfoStandWidgetRentableBotViewProps } from './InfoStandWidgetRentableBotView.types';

View File

@ -4,9 +4,9 @@ import { FC, FocusEvent, KeyboardEvent, useCallback, useEffect, useState } from
import { GetGroupInformation, LocalizeText, RoomWidgetChangeMottoMessage, RoomWidgetUpdateInfostandUserEvent } from '../../../../../../api';
import { CreateMessageHook, SendMessageHook } from '../../../../../../hooks';
import { CreateEventDispatcherHook } from '../../../../../../hooks/events';
import { UserProfileIconView } from '../../../../../../layout';
import { AvatarImageView } from '../../../../../shared/avatar-image/AvatarImageView';
import { BadgeImageView } from '../../../../../shared/badge-image/BadgeImageView';
import { UserProfileIconView } from '../../../../../shared/user-profile-icon/UserProfileIconView';
import { RelationshipsContainerView } from '../../../../../user-profile/views/relationships-container/RelationshipsContainerView';
import { useRoomContext } from '../../../../context/RoomContext';
import { InfoStandWidgetUserViewProps } from './InfoStandWidgetUserView.types';

View File

@ -0,0 +1,39 @@
.wordquiz-question {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
font-size: large;
background: rgba($dark, 0.9);
//box-shadow: inset 0px 5px lighten(rgba($dark,.6),2.5), inset 0 -4px darken(rgba($dark,.6),4);
border-radius: $border-radius;
transition: all 0.2s ease;
.question {
max-width: 300px;
}
}
.word-quiz-dislike {
background: url("../../../../assets/images/room-widgets/wordquiz-widget/thumbs-down.png");
width: 31px;
height: 34px;
}
.word-quiz-like {
background: url("../../../../assets/images/room-widgets/wordquiz-widget/thumbs-up.png");
width: 31px;
height: 34px;
}
.word-quiz-dislike-sm {
background: url("../../../../assets/images/room-widgets/wordquiz-widget/thumbs-down-small.png");
width: 22px;
height: 22px;
}
.word-quiz-like-sm {
background: url("../../../../assets/images/room-widgets/wordquiz-widget/thumbs-up-small.png");
height: 22px;
width: 22px;
}

View File

@ -0,0 +1,167 @@
import { IQuestion } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { RoomWidgetWordQuizUpdateEvent } from '../../../../api/nitro/room/widgets/events/RoomWidgetWordQuizUpdateEvent';
import { RoomWidgetPollMessage } from '../../../../api/nitro/room/widgets/messages/RoomWidgetPollMessage';
import { BatchUpdates, CreateEventDispatcherHook } from '../../../../hooks';
import { useRoomContext } from '../../context/RoomContext';
import { VALUE_KEY_DISLIKE, VALUE_KEY_LIKE, VoteValue } from './common/VoteValue';
import { QuestionView } from './views/question/QuestionView';
import { VoteView } from './views/vote/VoteView';
const DEFAULT_DISPLAY_DELAY = 4000;
const SIGN_FADE_DELAY = 3;
export const WordQuizWidgetView: FC<{}> = props =>
{
const { eventDispatcher = null, widgetHandler = null, roomSession = null } = useRoomContext();
const [pollId, setPollId] = useState(-1);
const [question, setQuestion] = useState<IQuestion>(null);
const [answerSent, setAnswerSent] = useState(false);
const [questionClearTimeout, setQuestionClearTimeout] = useState<number>(null);
const [answerCounts, setAnswerCounts] = useState<Map<string, number>>(new Map());
const [userAnswers, setUserAnswers] = useState<Map<number, VoteValue>>(new Map());
const clearQuestion = useCallback(() =>
{
setPollId(-1);
setQuestion(null);
}, []);
const onRoomWidgetWordQuizUpdateEvent = useCallback((event: RoomWidgetWordQuizUpdateEvent) =>
{
switch(event.type)
{
case RoomWidgetWordQuizUpdateEvent.NEW_QUESTION:
BatchUpdates(() =>
{
setPollId(event.id);
setQuestion(event.question);
setAnswerSent(false);
setAnswerCounts(new Map());
setUserAnswers(new Map());
if(questionClearTimeout) clearTimeout(questionClearTimeout);
});
if(event.duration > 0)
{
const delay = event.duration < 1000 ? DEFAULT_DISPLAY_DELAY : event.duration;
setQuestionClearTimeout(prevValue =>
{
if(prevValue) clearTimeout(prevValue);
return setTimeout((clearQuestion as TimerHandler), delay);
})
}
break;
case RoomWidgetWordQuizUpdateEvent.QUESTION_ANSWERED:
const userData = roomSession.userDataManager.getUserData(event.userId);
if(!userData) return;
setAnswerCounts(event.answerCounts);
if(!userAnswers.has(userData.roomIndex))
{
const answersCopy = new Map(userAnswers);
answersCopy.set(userData.roomIndex, { value: event.value, secondsLeft: SIGN_FADE_DELAY });
setUserAnswers(answersCopy);
}
break;
case RoomWidgetWordQuizUpdateEvent.QUESTION_FINISHED:
if(question && question.id === event.questionId)
{
BatchUpdates(() =>
{
setAnswerCounts(event.answerCounts);
setAnswerSent(true);
setQuestionClearTimeout(prevValue =>
{
if(prevValue) clearTimeout(prevValue);
return setTimeout((clearQuestion as TimerHandler), DEFAULT_DISPLAY_DELAY);
});
})
}
setUserAnswers(new Map());
break;
}
}, [clearQuestion, question, questionClearTimeout, roomSession.userDataManager, userAnswers]);
CreateEventDispatcherHook(RoomWidgetWordQuizUpdateEvent.NEW_QUESTION, eventDispatcher, onRoomWidgetWordQuizUpdateEvent);
CreateEventDispatcherHook(RoomWidgetWordQuizUpdateEvent.QUESTION_ANSWERED, eventDispatcher, onRoomWidgetWordQuizUpdateEvent);
CreateEventDispatcherHook(RoomWidgetWordQuizUpdateEvent.QUESTION_FINISHED, eventDispatcher, onRoomWidgetWordQuizUpdateEvent);
const vote = useCallback((vote: string) =>
{
if(answerSent || !question) return;
const updateMessage = new RoomWidgetPollMessage(RoomWidgetPollMessage.ANSWER, pollId);
updateMessage.questionId = question.id;
updateMessage.answers = [vote];
widgetHandler.processWidgetMessage(updateMessage);
setAnswerSent(true);
}, [answerSent, pollId, question, widgetHandler]);
const checkSignFade = useCallback(() =>
{
setUserAnswers(prev =>
{
const copy = new Map(prev);
const keysToRemove: number[] = [];
copy.forEach((value, key) =>
{
value.secondsLeft--;
if(value.secondsLeft <= 0)
{
keysToRemove.push(key);
}
});
keysToRemove.forEach(key => copy.delete(key));
return copy;
})
}, []);
useEffect(() =>
{
const interval = setInterval(() =>
{
checkSignFade();
}, 1000)
return () =>
{
clearInterval(interval);
}
}, [checkSignFade]);
useEffect(() =>
{
return () =>
{
setQuestionClearTimeout(prev =>
{
if(prev) clearTimeout(prev);
return null;
});
}
}, []);
return (
<>
{question &&
<QuestionView question={question.content} canVote={!answerSent} vote={vote} noVotes={answerCounts.get(VALUE_KEY_DISLIKE) || 0} yesVotes={answerCounts.get(VALUE_KEY_LIKE) || 0} />
}
{userAnswers &&
Array.from(userAnswers.entries()).map(([key, value], index) =>
{
return <VoteView key={index} userIndex={key} vote={value.value} />
})
}
</>
);
}

View File

@ -0,0 +1,8 @@
export const VALUE_KEY_DISLIKE = '0';
export const VALUE_KEY_LIKE = '1';
export interface VoteValue
{
value: string;
secondsLeft: number;
}

View File

@ -0,0 +1,24 @@
import { FC } from 'react';
import { VALUE_KEY_DISLIKE, VALUE_KEY_LIKE } from '../../common/VoteValue';
import { QuestionViewProps } from './QuestionView.types';
export const QuestionView:FC<QuestionViewProps> = props =>
{
const { question = null, canVote = null, vote = null, noVotes = null, yesVotes = null } = props;
return (
<div className="wordquiz-question p-2 d-flex flex-column gap-2">
<div className="w-100 d-flex align-items-center gap-2">
{ !canVote && <button className="btn btn-danger btn-sm">{noVotes}</button> }
<div className="question text-center text-break">{question}</div>
{ !canVote && <button className="btn btn-success btn-sm">{yesVotes}</button> }
</div>
{canVote &&
<div className="w-100 d-flex justify-content-center gap-2">
<button className="btn btn-danger btn-sm"><div className="word-quiz-dislike" onClick={ () => vote(VALUE_KEY_DISLIKE) }/></button>
<button className="btn btn-success btn-sm"><div className="word-quiz-like" onClick={ () => vote(VALUE_KEY_LIKE) }/></button>
</div>
}
</div>
)
}

View File

@ -0,0 +1,9 @@
export interface QuestionViewProps
{
question: string;
canVote: boolean;
vote(value: string): void;
noVotes: number;
yesVotes: number;
}

View File

@ -0,0 +1,16 @@
import { RoomObjectCategory } from '@nitrots/nitro-renderer/src';
import { FC } from 'react';
import { ObjectLocationView } from '../../../object-location/ObjectLocationView';
import { VALUE_KEY_DISLIKE } from '../../common/VoteValue';
import { VoteViewProps } from './VoteView.types';
export const VoteView:FC<VoteViewProps> = props =>
{
const { userIndex = null , vote = null } = props;
return (
<ObjectLocationView objectId={userIndex} category={RoomObjectCategory.UNIT}>
<button className={`btn btn-${(vote === VALUE_KEY_DISLIKE) ? 'danger' : 'success'} btn-sm px-1`}><div className={`word-quiz-${(vote === VALUE_KEY_DISLIKE) ? 'dislike' : 'like'}-sm`} /></button>
</ObjectLocationView>
)
}

View File

@ -0,0 +1,5 @@
export interface VoteViewProps
{
userIndex: number;
vote: string;
}

View File

@ -1,12 +0,0 @@
import { FC } from 'react';
import { GetUserProfile } from '../../../api';
import { UserProfileIconViewProps } from './UserProfileIconView.types';
export const UserProfileIconView: FC<UserProfileIconViewProps> = props =>
{
const { userId = 0, userName = null } = props;
return (<>
<i className="icon icon-user-profile me-1 cursor-pointer" onClick={ () => GetUserProfile(userId) } />
</>);
}

View File

@ -1,5 +0,0 @@
export interface UserProfileIconViewProps
{
userId?: number;
userName?: string;
}