GuideToolView.tsx 22.6 Ko
Télécharger le fichier
import {
AddLinkEventTracker,
GetSessionDataManager,
GuideOnDutyStatusMessageEvent,
GuideSessionAttachedMessageEvent,
GuideSessionCreateMessageComposer,
GuideSessionDetachedMessageEvent,
GuideSessionEndedMessageEvent,
GuideSessionErrorMessageEvent,
GuideSessionInvitedToGuideRoomMessageEvent,
GuideSessionMessageMessageEvent,
GuideSessionOnDutyUpdateMessageComposer,
GuideSessionPartnerIsTypingMessageEvent,
GuideSessionStartedMessageEvent,
ILinkEventTracker,
PerkAllowancesMessageEvent,
PerkEnum,
RemoveLinkEventTracker
} from '@octane/renderer';
import { FC, useCallback, useEffect, useRef, useState } from 'react';
import { GetConfigurationValue, guardianText, GuideSessionState, GuideToolMessage, GuideToolMessageGroup, isSafeExternalUrl, LocalizeText, SendMessageComposer } from '../../api';
import { OctaneCardContentView, OctaneCardHeaderView, OctaneCardView } from '../../common';
import { GuideToolEvent } from '../../events';
import { useGuardianChatReview, useMessageEvent, useNotification, useUiEvent } from '../../hooks';
import { GuardianChatReviewAcceptView } from './views/GuardianChatReviewAcceptView';
import { GuardianChatReviewResultsView } from './views/GuardianChatReviewResultsView';
import { GuardianChatReviewVoteView } from './views/GuardianChatReviewVoteView';
import { GuideToolAcceptView } from './views/GuideToolAcceptView';
import { GuideToolMenuView } from './views/GuideToolMenuView';
import { GuideToolOngoingView } from './views/GuideToolOngoingView';
import { GuideToolReportView } from './views/GuideToolReportView';
import { GuideToolUserCreateRequestView } from './views/GuideToolUserCreateRequestView';
import { GuideToolUserFeedbackView } from './views/GuideToolUserFeedbackView';
import { GuideToolUserNoHelpersView } from './views/GuideToolUserNoHelpersView';
import { GuideToolUserPendingView } from './views/GuideToolUserPendingView';
import { GuideToolUserSomethingWrogView } from './views/GuideToolUserSomethingWrogView';
import { GuideToolUserThanksView } from './views/GuideToolUserThanksView';
import { NativeTextTree } from '../../common/native-text/NativeTextTree';
export const GuideToolView: FC<{}> = (props) => {
const [isVisible, setIsVisible] = useState<boolean>(false);
const [headerText, setHeaderText] = useState<string>(LocalizeText('guide.help.guide.tool.title'));
const [noCloseButton, setNoCloseButton] = useState<boolean>(false);
const [sessionState, setSessionState] = useState<string>(GuideSessionState.GUIDE_TOOL_MENU);
const [isOnDuty, setIsOnDuty] = useState<boolean>(false);
const isOnDutyRef = useRef(false);
isOnDutyRef.current = isOnDuty;
const [isHandlingBullyReports, setIsHandlingBullyReports] = useState<boolean>(false);
const [isHandlingGuideRequests, setIsHandlingGuideRequests] = useState<boolean>(false);
const [isHandlingHelpRequests, setIsHandlingHelpRequests] = useState<boolean>(false);
const [helpersOnDuty, setHelpersOnDuty] = useState<number>(0);
const [guidesOnDuty, setGuidesOnDuty] = useState<number>(0);
const [guardiansOnDuty, setGuardiansOnDuty] = useState<number>(0);
const [userRequest, setUserRequest] = useState<string>('');
const [helpRequestDescription, setHelpRequestDescription] = useState<string>(null);
const [helpRequestAverageTime, setHelpRequestAverageTime] = useState<number>(0);
const [ongoingUserId, setOngoingUserId] = useState<number>(0);
const [ongoingUsername, setOngoingUsername] = useState<string>(null);
const [ongoingFigure, setOngoingFigure] = useState<string>(null);
const [ongoingIsTyping, setOngoingIsTyping] = useState<boolean>(false);
const [ongoingMessageGroups, setOngoingMessageGroups] = useState<GuideToolMessageGroup[]>([]);
const [isReporting, setIsReporting] = useState<boolean>(false);
const [canJudgeChatReviews, setCanJudgeChatReviews] = useState<boolean>(true);
const { simpleAlert = null } = useNotification();
const { review, accept: acceptReview, skip: skipReview, vote: voteReview, close: closeReview } = useGuardianChatReview();
// A review offered to the guardian opens the guide tool on it, as AIR opens the chat review window.
useEffect(() => {
if (review.state) setIsVisible(true);
}, [review.state]);
const updateSessionState = useCallback((newState: string, replacement?: string) => {
switch (newState) {
case GuideSessionState.GUIDE_TOOL_MENU:
setHeaderText(LocalizeText('guide.help.guide.tool.title'));
setNoCloseButton(false);
break;
case GuideSessionState.GUIDE_ACCEPT:
setHeaderText(LocalizeText('guide.help.request.guide.accept.title'));
setNoCloseButton(true);
break;
case GuideSessionState.GUIDE_ONGOING:
setHeaderText(LocalizeText('guide.help.request.guide.ongoing.title', ['name'], [replacement]));
setNoCloseButton(true);
break;
case GuideSessionState.USER_CREATE:
setHeaderText(LocalizeText('guide.help.request.user.create.title'));
setNoCloseButton(false);
break;
case GuideSessionState.USER_PENDING:
setHeaderText(LocalizeText('guide.help.request.user.pending.title'));
setNoCloseButton(true);
break;
case GuideSessionState.USER_ONGOING:
setHeaderText(LocalizeText('guide.help.request.user.ongoing.title', ['name'], [replacement]));
setNoCloseButton(true);
break;
case GuideSessionState.USER_FEEDBACK:
setHeaderText(LocalizeText('guide.help.request.user.feedback.title'));
setNoCloseButton(true);
break;
case GuideSessionState.USER_THANKS:
setHeaderText(LocalizeText('guide.help.request.user.thanks.title'));
setNoCloseButton(false);
break;
case GuideSessionState.USER_NO_HELPERS:
setHeaderText(LocalizeText('guide.help.request.no_tour_guides.heading'));
setNoCloseButton(false);
break;
case GuideSessionState.USER_SOMETHING_WRONG:
setHeaderText(LocalizeText('guide.help.request.user.guide.disconnected.error.heading'));
setNoCloseButton(false);
break;
}
setSessionState(newState);
setIsVisible(true);
}, []);
const onGuideToolEvent = useCallback(
(event: GuideToolEvent) => {
switch (event.type) {
case GuideToolEvent.SHOW_GUIDE_TOOL:
setIsVisible(true);
return;
case GuideToolEvent.HIDE_GUIDE_TOOL:
setIsVisible(false);
return;
case GuideToolEvent.TOGGLE_GUIDE_TOOL:
setIsVisible((value) => !value);
return;
case GuideToolEvent.CREATE_HELP_REQUEST:
updateSessionState(GuideSessionState.USER_CREATE);
return;
}
},
[updateSessionState]
);
useUiEvent(GuideToolEvent.SHOW_GUIDE_TOOL, onGuideToolEvent);
useUiEvent(GuideToolEvent.HIDE_GUIDE_TOOL, onGuideToolEvent);
useUiEvent(GuideToolEvent.TOGGLE_GUIDE_TOOL, onGuideToolEvent);
useUiEvent(GuideToolEvent.CREATE_HELP_REQUEST, onGuideToolEvent);
useMessageEvent<PerkAllowancesMessageEvent>(PerkAllowancesMessageEvent, (event) => {
const parser = event.getParser();
setCanJudgeChatReviews(parser.isAllowed(PerkEnum.JUDGE_CHAT_REVIEWS));
if (!parser.isAllowed(PerkEnum.USE_GUIDE_TOOL) && isOnDuty) {
setIsOnDuty(false);
SendMessageComposer(new GuideSessionOnDutyUpdateMessageComposer(false, false, false, false));
}
});
useMessageEvent<GuideOnDutyStatusMessageEvent>(GuideOnDutyStatusMessageEvent, (event) => {
const parser = event.getParser();
setIsOnDuty(parser.onDuty);
setGuidesOnDuty(parser.guidesOnDuty);
setHelpersOnDuty(parser.helpersOnDuty);
setGuardiansOnDuty(parser.guardiansOnDuty);
});
useMessageEvent<GuideSessionAttachedMessageEvent>(GuideSessionAttachedMessageEvent, (event) => {
const parser = event.getParser();
setHelpRequestDescription(parser.helpRequestDescription);
setHelpRequestAverageTime(parser.roleSpecificWaitTime);
if (parser.asGuide && isOnDuty) updateSessionState(GuideSessionState.GUIDE_ACCEPT);
if (!parser.asGuide) updateSessionState(GuideSessionState.USER_PENDING);
});
useMessageEvent<GuideSessionStartedMessageEvent>(GuideSessionStartedMessageEvent, (event) => {
const parser = event.getParser();
if (isOnDuty) {
setOngoingUserId(parser.requesterUserId);
setOngoingUsername(parser.requesterName);
setOngoingFigure(parser.requesterFigure);
updateSessionState(GuideSessionState.GUIDE_ONGOING, parser.requesterName);
} else {
setOngoingUserId(parser.guideUserId);
setOngoingUsername(parser.guideName);
setOngoingFigure(parser.guideFigure);
updateSessionState(GuideSessionState.USER_ONGOING, parser.guideName);
}
});
useMessageEvent<GuideSessionPartnerIsTypingMessageEvent>(GuideSessionPartnerIsTypingMessageEvent, (event) => {
const parser = event.getParser();
setOngoingIsTyping(parser.isTyping);
});
useMessageEvent<GuideSessionMessageMessageEvent>(GuideSessionMessageMessageEvent, (event) => {
const parser = event.getParser();
const messageGroups = [...ongoingMessageGroups];
let lastGroup = messageGroups[messageGroups.length - 1];
if (!lastGroup || lastGroup.userId !== parser.senderId) {
lastGroup = new GuideToolMessageGroup(parser.senderId);
messageGroups.push(lastGroup);
}
lastGroup.addChat(new GuideToolMessage(parser.chatMessage));
setOngoingMessageGroups(messageGroups);
});
useMessageEvent<GuideSessionInvitedToGuideRoomMessageEvent>(GuideSessionInvitedToGuideRoomMessageEvent, (event) => {
const parser = event.getParser();
if (parser.roomId !== 0) {
const messageGroups = [...ongoingMessageGroups];
let lastGroup = messageGroups[messageGroups.length - 1];
const guideId = isOnDuty ? GetSessionDataManager().userId : ongoingUserId;
if (!lastGroup || lastGroup.userId !== guideId) {
lastGroup = new GuideToolMessageGroup(guideId);
messageGroups.push(lastGroup);
}
lastGroup.addChat(new GuideToolMessage(parser.roomName, parser.roomId));
setOngoingMessageGroups(messageGroups);
}
});
useMessageEvent<GuideSessionEndedMessageEvent>(GuideSessionEndedMessageEvent, (event) => {
if (isOnDuty) {
setOngoingUserId(0);
setOngoingUsername(null);
setOngoingFigure(null);
setOngoingIsTyping(false);
setOngoingMessageGroups([]);
updateSessionState(GuideSessionState.GUIDE_TOOL_MENU);
} else {
updateSessionState(GuideSessionState.USER_FEEDBACK);
}
});
useMessageEvent<GuideSessionErrorMessageEvent>(GuideSessionErrorMessageEvent, (event) => {
const parser = event.getParser();
// SOMETHING_WRONG_REQUEST = 0, NO_HELPERS_AVAILABLE = 1, NO_GUARDIANS_AVAILABLE = 2 / 3
switch (parser['errorCode']) {
case 0:
updateSessionState(GuideSessionState.USER_SOMETHING_WRONG);
break;
case 1:
updateSessionState(GuideSessionState.USER_NO_HELPERS);
break;
case 2:
case 3:
// AIR setStateClosedWithNotification: the bully report went to the moderators instead.
simpleAlert(
guardianText('guide.bully.request.error.not_enough_guardians.message'),
null,
null,
null,
guardianText('guide.bully.request.error.not_enough_guardians.title')
);
break;
}
});
useMessageEvent<GuideSessionDetachedMessageEvent>(GuideSessionDetachedMessageEvent, (event) => {
setOngoingUserId(0);
setOngoingUsername(null);
setOngoingFigure(null);
setOngoingIsTyping(false);
setOngoingMessageGroups([]);
if (isOnDuty) {
updateSessionState(GuideSessionState.GUIDE_TOOL_MENU);
} else {
updateSessionState(GuideSessionState.USER_THANKS);
}
});
useEffect(() => {
const linkTracker: ILinkEventTracker = {
linkReceived: (url: string) => {
const parts = url.split('/');
if (parts.length < 2) return;
switch (parts[1]) {
case 'tour':
// Official HabboHelp.requestGuide: a tour request (type 0) with the tour text.
if (!GetConfigurationValue<boolean>('guides.enabled')) return;
SendMessageComposer(new GuideSessionCreateMessageComposer(0, LocalizeText('guide.help.request.tour.description')));
return;
}
},
eventUrlPrefix: 'help/'
};
AddLinkEventTracker(linkTracker);
return () => RemoveLinkEventTracker(linkTracker);
}, []);
const processAction = useCallback(
(action: string) => {
switch (action) {
case 'close':
setIsVisible(false);
setIsReporting(false);
setUserRequest('');
setSessionState(GuideSessionState.GUIDE_TOOL_MENU);
return;
case 'toggle_duty':
if (!isHandlingBullyReports && !isHandlingGuideRequests && !isHandlingHelpRequests) {
simpleAlert(
LocalizeText('guide.help.guide.tool.noqueueselected.message'),
null,
null,
null,
LocalizeText('guide.help.guide.tool.noqueueselected.caption'),
null
);
return;
}
// Toggled from a ref, not inside a state updater: an updater may run twice (StrictMode).
const wasOnDuty = isOnDutyRef.current;
isOnDutyRef.current = !wasOnDuty;
setIsOnDuty(!wasOnDuty);
SendMessageComposer(
new GuideSessionOnDutyUpdateMessageComposer(
!wasOnDuty,
wasOnDuty ? false : isHandlingGuideRequests,
wasOnDuty ? false : isHandlingHelpRequests,
wasOnDuty ? false : isHandlingBullyReports
)
);
return;
case 'forum_link':
const url: string = GetConfigurationValue<string>('group.homepage.url', '').replace(
'%groupid%',
GetConfigurationValue<string>('guide.help.alpha.groupid', '0')
);
// A hotel without a group homepage set opened an empty tab; only a web URL is opened.
if (isSafeExternalUrl(url)) window.open(url, '_blank', 'noopener,noreferrer');
return;
}
},
[isHandlingBullyReports, isHandlingGuideRequests, isHandlingHelpRequests, simpleAlert]
);
if (!isVisible) return null;
if (review.state) {
const reviewHeader: Record<string, string> = {
[GuideSessionState.GUARDIAN_CHAT_REVIEW_ACCEPT]: 'guide.bully.request.guide.accept.title',
[GuideSessionState.GUARDIAN_CHAT_REVIEW_WAIT_FOR_VOTERS]: 'guide.bully.request.guide.vote.title',
[GuideSessionState.GUARDIAN_CHAT_REVIEW_VOTE]: 'guide.bully.request.guide.vote.title',
[GuideSessionState.GUARDIAN_CHAT_REVIEW_WAIT_FOR_RESULTS]: 'guide.bully.request.guide.results.title',
[GuideSessionState.GUARDIAN_CHAT_REVIEW_RESULTS]: 'guide.bully.request.guide.results.final.title'
};
// Only the two result windows have a close button (AIR openWindow(..., true)).
const reviewClosable =
review.state === GuideSessionState.GUARDIAN_CHAT_REVIEW_WAIT_FOR_RESULTS || review.state === GuideSessionState.GUARDIAN_CHAT_REVIEW_RESULTS;
return (
<NativeTextTree>
<OctaneCardView className="octane-guide-tool octane-guardian-review" theme="primary-slim">
<OctaneCardHeaderView headerText={guardianText(reviewHeader[review.state])} noCloseButton={!reviewClosable} onCloseClick={closeReview} />
<OctaneCardContentView className="text-black">
{review.state === GuideSessionState.GUARDIAN_CHAT_REVIEW_ACCEPT && (
<GuardianChatReviewAcceptView
countdownId={review.countdownId}
countdownSeconds={review.countdownSeconds}
onAccept={acceptReview}
onSkip={skipReview}
/>
)}
{(review.state === GuideSessionState.GUARDIAN_CHAT_REVIEW_WAIT_FOR_VOTERS || review.state === GuideSessionState.GUARDIAN_CHAT_REVIEW_VOTE) && (
<GuardianChatReviewVoteView
countdownId={review.countdownId}
countdownSeconds={review.countdownSeconds}
record={review.record}
waiting={review.state === GuideSessionState.GUARDIAN_CHAT_REVIEW_WAIT_FOR_VOTERS}
onClose={closeReview}
onVote={voteReview}
/>
)}
{(review.state === GuideSessionState.GUARDIAN_CHAT_REVIEW_WAIT_FOR_RESULTS || review.state === GuideSessionState.GUARDIAN_CHAT_REVIEW_RESULTS) && (
<GuardianChatReviewResultsView
final={review.state === GuideSessionState.GUARDIAN_CHAT_REVIEW_RESULTS}
ownVote={review.ownVote}
statuses={review.statuses}
winningVote={review.winningVote}
onClose={closeReview}
/>
)}
</OctaneCardContentView>
</OctaneCardView>
</NativeTextTree>
);
}
// AIR offers "report" in the ongoing windows of both sides and in the user's feedback window.
const canReport = [GuideSessionState.GUIDE_ONGOING, GuideSessionState.USER_ONGOING, GuideSessionState.USER_FEEDBACK].includes(sessionState);
return (
<NativeTextTree>
<>
{isReporting && canReport && <GuideToolReportView onClose={() => setIsReporting(false)} />}
<OctaneCardView className="octane-guide-tool" theme="primary-slim">
<OctaneCardHeaderView headerText={headerText} noCloseButton={noCloseButton} onCloseClick={(event) => processAction('close')} />
<OctaneCardContentView className="text-black">
{sessionState === GuideSessionState.GUIDE_TOOL_MENU && (
<GuideToolMenuView
canJudgeChatReviews={canJudgeChatReviews}
guardiansOnDuty={guardiansOnDuty}
guidesOnDuty={guidesOnDuty}
helpersOnDuty={helpersOnDuty}
isHandlingBullyReports={isHandlingBullyReports}
isHandlingGuideRequests={isHandlingGuideRequests}
isHandlingHelpRequests={isHandlingHelpRequests}
isOnDuty={isOnDuty}
processAction={processAction}
setIsHandlingBullyReports={setIsHandlingBullyReports}
setIsHandlingGuideRequests={setIsHandlingGuideRequests}
setIsHandlingHelpRequests={setIsHandlingHelpRequests}
/>
)}
{sessionState === GuideSessionState.GUIDE_ACCEPT && (
<GuideToolAcceptView helpRequestAverageTime={helpRequestAverageTime} helpRequestDescription={helpRequestDescription} />
)}
{[GuideSessionState.GUIDE_ONGOING, GuideSessionState.USER_ONGOING].includes(sessionState) && (
<GuideToolOngoingView
isGuide={isOnDuty}
isTyping={ongoingIsTyping}
messageGroups={ongoingMessageGroups}
userFigure={ongoingFigure}
userId={ongoingUserId}
userName={ongoingUsername}
onReport={() => setIsReporting(true)}
/>
)}
{sessionState === GuideSessionState.USER_CREATE && (
<GuideToolUserCreateRequestView setUserRequest={setUserRequest} userRequest={userRequest} />
)}
{sessionState === GuideSessionState.USER_PENDING && (
<GuideToolUserPendingView helpRequestAverageTime={helpRequestAverageTime} helpRequestDescription={helpRequestDescription} />
)}
{sessionState === GuideSessionState.USER_FEEDBACK && (
<GuideToolUserFeedbackView userName={ongoingUsername} onReport={() => setIsReporting(true)} />
)}
{sessionState === GuideSessionState.USER_THANKS && <GuideToolUserThanksView />}
{sessionState === GuideSessionState.USER_NO_HELPERS && <GuideToolUserNoHelpersView />}
{sessionState === GuideSessionState.USER_SOMETHING_WRONG && <GuideToolUserSomethingWrogView />}
</OctaneCardContentView>
</OctaneCardView>
</>
</NativeTextTree>
);
};
