Merge branch 'feature/mod-tools' into dev

This commit is contained in:
Bill 2021-07-21 17:52:45 -04:00
commit e2cc64fe6d
23 changed files with 615 additions and 6 deletions

View File

@ -0,0 +1,10 @@
import { NitroEvent } from 'nitro-renderer';
export class ModToolsEvent extends NitroEvent
{
public static SHOW_MOD_TOOLS: string = 'MTE_SHOW_MOD_TOOLS';
public static HIDE_MOD_TOOLS: string = 'MTE_HIDE_MOD_TOOLS';
public static TOGGLE_MOD_TOOLS: string = 'MTE_TOGGLE_MOD_TOOLS';
public static SELECT_USER: string = 'MTE_SELECT_USER';
public static OPEN_ROOM_INFO: string = 'MTE_OPEN_ROOM_INFO';
}

View File

@ -0,0 +1,18 @@
import { ModToolsEvent } from './ModToolsEvent';
export class ModToolsOpenRoomInfoEvent extends ModToolsEvent
{
private _roomId: number;
constructor(roomId: number)
{
super(ModToolsEvent.OPEN_ROOM_INFO);
this._roomId = roomId;
}
public get roomId(): number
{
return this._roomId;
}
}

View File

@ -0,0 +1,25 @@
import { ModToolsEvent } from './ModToolsEvent';
export class ModToolsSelectUserEvent extends ModToolsEvent
{
private _webID: number;
private _name: string;
constructor(webID: number, name: string)
{
super(ModToolsEvent.SELECT_USER);
this._webID = webID;
this._name = name;
}
public get webID(): number
{
return this._webID;
}
public get name(): string
{
return this._name;
}
}

View File

@ -15,3 +15,4 @@
@import './room-host/RoomHostView';
@import './toolbar/ToolbarView';
@import './wired/WiredView';
@import './mod-tools/ModToolsView';

View File

@ -6,6 +6,7 @@ import { CatalogView } from '../catalog/CatalogView';
import { FriendListView } from '../friend-list/FriendListView';
import { HotelView } from '../hotel-view/HotelView';
import { InventoryView } from '../inventory/InventoryView';
import { ModToolsView } from '../mod-tools/ModToolsView';
import { NavigatorView } from '../navigator/NavigatorView';
import { NotificationCenterView } from '../notification-center/NotificationCenterView';
import { RightSideView } from '../right-side/RightSideView';
@ -46,6 +47,7 @@ export const MainView: FC<MainViewProps> = props =>
<div className="nitro-main">
{ landingViewVisible && <HotelView /> }
<ToolbarView isInRoom={ !landingViewVisible } />
<ModToolsView />
<RoomHostView />
<WiredView />
<AvatarEditorView />

View File

@ -0,0 +1,6 @@
.nitro-mod-tools {
width: 200px;
}
@import './views/chatlog/ModToolsChatlogView';
@import './views/room/ModToolsRoomView';

View File

@ -0,0 +1,182 @@
import { RoomEngineEvent } from 'nitro-renderer';
import { FC, useCallback, useEffect, useReducer, useState } from 'react';
import { ModToolsEvent } from '../../events/mod-tools/ModToolsEvent';
import { ModToolsOpenRoomInfoEvent } from '../../events/mod-tools/ModToolsOpenRoomInfoEvent';
import { ModToolsSelectUserEvent } from '../../events/mod-tools/ModToolsSelectUserEvent';
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 { ModToolsViewProps } from './ModToolsView.types';
import { initialModTools, ModToolsActions, ModToolsReducer } from './reducers/ModToolsReducer';
import { ModToolsRoomView } from './views/room/ModToolsRoomView';
import { ModToolsTicketsView } from './views/tickets/ModToolsTicketsView';
import { ModToolsUserView } from './views/user/ModToolsUserView';
export const ModToolsView: FC<ModToolsViewProps> = props =>
{
const [ isVisible, setIsVisible ] = useState(false);
const [ modToolsState, dispatchModToolsState ] = useReducer(ModToolsReducer, initialModTools);
const { currentRoomId = null, selectedUser = null, openRooms = null, openChatlogs = null } = modToolsState;
const [ isUserVisible, setIsUserVisible ] = useState(false);
const [ isTicketsVisible, setIsTicketsVisible ] = useState(false);
const onModToolsEvent = useCallback((event: ModToolsEvent) =>
{
switch(event.type)
{
case ModToolsEvent.SHOW_MOD_TOOLS:
setIsVisible(true);
return;
case ModToolsEvent.HIDE_MOD_TOOLS:
setIsVisible(false);
return;
case ModToolsEvent.TOGGLE_MOD_TOOLS:
setIsVisible(value => !value);
return;
case ModToolsEvent.SELECT_USER: {
const castedEvent = (event as ModToolsSelectUserEvent);
dispatchModToolsState({
type: ModToolsActions.SET_SELECTED_USER,
payload: {
selectedUser: {
webID: castedEvent.webID,
name: castedEvent.name
}
}
});
return;
}
case ModToolsEvent.OPEN_ROOM_INFO: {
const castedEvent = (event as ModToolsOpenRoomInfoEvent);
if(openRooms && openRooms.includes(castedEvent.roomId)) return;
dispatchModToolsState({
type: ModToolsActions.SET_OPEN_ROOMS,
payload: {
openRooms: [...openRooms, castedEvent.roomId]
}
});
return;
}
}
}, [ dispatchModToolsState, setIsVisible, openRooms ]);
useUiEvent(ModToolsEvent.SHOW_MOD_TOOLS, onModToolsEvent);
useUiEvent(ModToolsEvent.HIDE_MOD_TOOLS, onModToolsEvent);
useUiEvent(ModToolsEvent.TOGGLE_MOD_TOOLS, onModToolsEvent);
useUiEvent(ModToolsEvent.SELECT_USER, onModToolsEvent);
useUiEvent(ModToolsEvent.OPEN_ROOM_INFO, onModToolsEvent);
const onRoomEngineEvent = useCallback((event: RoomEngineEvent) =>
{
switch(event.type)
{
case RoomEngineEvent.INITIALIZED:
dispatchModToolsState({
type: ModToolsActions.SET_CURRENT_ROOM_ID,
payload: {
currentRoomId: event.roomId
}
});
return;
case RoomEngineEvent.DISPOSED:
dispatchModToolsState({
type: ModToolsActions.SET_CURRENT_ROOM_ID,
payload: {
currentRoomId: null
}
});
return;
}
}, [ dispatchModToolsState ]);
useRoomEngineEvent(RoomEngineEvent.INITIALIZED, onRoomEngineEvent);
useRoomEngineEvent(RoomEngineEvent.DISPOSED, onRoomEngineEvent);
const handleClick = useCallback((action: string, value?: string) =>
{
if(!action) return;
switch(action)
{
case 'toggle_room': {
if(!openRooms)
{
dispatchUiEvent(new ModToolsOpenRoomInfoEvent(currentRoomId));
return;
}
const itemIndex = openRooms.indexOf(currentRoomId);
if(itemIndex > -1)
{
handleClick('close_room', currentRoomId.toString());
}
else
{
dispatchUiEvent(new ModToolsOpenRoomInfoEvent(currentRoomId));
}
return;
}
case 'close_room': {
const itemIndex = openRooms.indexOf(Number(value));
const clone = Array.from(openRooms);
clone.splice(itemIndex, 1);
dispatchModToolsState({
type: ModToolsActions.SET_OPEN_ROOMS,
payload: {
openRooms: clone
}
});
return;
}
case 'close_chatlog': {
const itemIndex = openChatlogs.indexOf(Number(value));
const clone = Array.from(openChatlogs);
clone.splice(itemIndex, 1);
dispatchModToolsState({
type: ModToolsActions.SET_OPEN_CHATLOGS,
payload: {
openChatlogs: clone
}
});
return;
}
}
}, [ dispatchModToolsState, openRooms, openChatlogs, currentRoomId ]);
useEffect(() =>
{
if(!isVisible) return;
}, [ isVisible ]);
return (
<ModToolsContextProvider value={ { modToolsState, dispatchModToolsState } }>
{ isVisible &&
<NitroCardView className="nitro-mod-tools" simple={ true }>
<NitroCardHeaderView headerText={ "Mod Tools" } onCloseClick={ event => setIsVisible(false) } />
<NitroCardContentView className="text-black">
<button className="btn btn-primary w-100 mb-2" onClick={ () => handleClick('toggle_room') } disabled={ !currentRoomId }><i className="fas fa-home"></i> Room Tool</button>
<button className="btn btn-primary w-100 mb-2" onClick={ () => {} } disabled={ !currentRoomId }><i className="fas fa-comments"></i> Chatlog Tool</button>
<button className="btn btn-primary w-100 mb-2" onClick={ () => setIsUserVisible(value => !value) } disabled={ !selectedUser }><i className="fas fa-user"></i> User: { selectedUser ? selectedUser.name : '' }</button>
<button className="btn btn-primary w-100" onClick={ () => setIsTicketsVisible(value => !value) }><i className="fas fa-exclamation-circle"></i> Report Tool</button>
</NitroCardContentView>
</NitroCardView> }
{ openRooms && openRooms.map(roomId =>
{
return <ModToolsRoomView key={ roomId } roomId={ roomId } onCloseClick={ () => handleClick('close_room', roomId.toString()) } />;
}) }
{ isUserVisible && <ModToolsUserView /> }
{ isTicketsVisible && <ModToolsTicketsView /> }
</ModToolsContextProvider>
);
}

View File

@ -0,0 +1,2 @@
export interface ModToolsViewProps
{}

View File

@ -0,0 +1,14 @@
import { createContext, FC, useContext } from 'react';
import { IModToolsContext, ModToolsContextProps } from './ModToolsContext.types';
const ModToolsContext = createContext<IModToolsContext>({
modToolsState: null,
dispatchModToolsState: null
});
export const ModToolsContextProvider: FC<ModToolsContextProps> = props =>
{
return <ModToolsContext.Provider value={ props.value }>{ props.children }</ModToolsContext.Provider>
}
export const useModToolsContext = () => useContext(ModToolsContext);

View File

@ -0,0 +1,13 @@
import { Dispatch, ProviderProps } from 'react';
import { IModToolsAction, IModToolsState } from '../reducers/ModToolsReducer';
export interface IModToolsContext
{
modToolsState: IModToolsState;
dispatchModToolsState: Dispatch<IModToolsAction>;
}
export interface ModToolsContextProps extends ProviderProps<IModToolsContext>
{
}

View File

@ -0,0 +1,63 @@
import { Reducer } from 'react';
export interface IModToolsState
{
selectedUser: {webID: number, name: string};
currentRoomId: number;
openRooms: number[];
openChatlogs: number[];
}
export interface IModToolsAction
{
type: string;
payload: {
selectedUser?: {webID: number, name: string};
currentRoomId?: number;
openRooms?: number[];
openChatlogs?: number[];
}
}
export class ModToolsActions
{
public static SET_SELECTED_USER: string = 'MTA_SET_SELECTED_USER';
public static SET_CURRENT_ROOM_ID: string = 'MTA_SET_CURRENT_ROOM_ID';
public static SET_OPEN_ROOMS: string = 'MTA_SET_OPEN_ROOMS';
public static SET_OPEN_CHATLOGS: string = 'MTA_SET_OPEN_CHATLOGS';
public static RESET_STATE: string = 'MTA_RESET_STATE';
}
export const initialModTools: IModToolsState = {
selectedUser: null,
currentRoomId: null,
openRooms: null,
openChatlogs: null
};
export const ModToolsReducer: Reducer<IModToolsState, IModToolsAction> = (state, action) =>
{
switch(action.type)
{
case ModToolsActions.SET_SELECTED_USER: {
const selectedUser = (action.payload.selectedUser || state.selectedUser || null);
return { ...state, selectedUser };
}
case ModToolsActions.SET_CURRENT_ROOM_ID: {
const currentRoomId = (action.payload.currentRoomId || state.currentRoomId || null);
return { ...state, currentRoomId };
}
case ModToolsActions.SET_OPEN_ROOMS: {
const openRooms = (action.payload.openRooms || state.openRooms || null);
return { ...state, openRooms };
}
case ModToolsActions.RESET_STATE: {
return { ...initialModTools };
}
default:
return state;
}
}

View File

@ -0,0 +1,26 @@
.nitro-mod-tools-chatlog {
width: 480px;
.chatlog-messages {
height: 300px;
max-height: 300px;
.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);
}
td {
padding: 0px 5px;
}
}
}
}

View File

@ -0,0 +1,84 @@
import { ModtoolRequestRoomChatlogComposer, ModtoolRoomChatlogEvent, ModtoolRoomChatlogLine } from 'nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { TryVisitRoom } from '../../../../api/navigator/TryVisitRoom';
import { CreateMessageHook, SendMessageHook } from '../../../../hooks/messages';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { ModToolsChatlogViewProps } from './ModToolsChatlogView.types';
export const ModToolsChatlogView: FC<ModToolsChatlogViewProps> = props =>
{
const { roomId = null, onCloseClick = null } = props;
const [ roomName, setRoomName ] = useState(null);
const [ messages, setMessages ] = useState<ModtoolRoomChatlogLine[]>(null);
const [ loadedRoomId, setLoadedRoomId ] = useState(null);
const [ messagesRequested, setMessagesRequested ] = useState(false);
useEffect(() =>
{
if(messagesRequested) return;
SendMessageHook(new ModtoolRequestRoomChatlogComposer(roomId));
setMessagesRequested(true);
}, [ roomId, messagesRequested, setMessagesRequested ]);
const onModtoolRoomChatlogEvent = useCallback((event: ModtoolRoomChatlogEvent) =>
{
const parser = event.getParser();
setRoomName(parser.name);
setMessages(parser.chatlogs);
setLoadedRoomId(parser.id);
}, [ setRoomName, setMessages ]);
CreateMessageHook(ModtoolRoomChatlogEvent, onModtoolRoomChatlogEvent);
const handleClick = useCallback((action: string, value?: string) =>
{
if(!action) return;
switch(action)
{
case 'close':
onCloseClick();
return;
case 'visit_room':
TryVisitRoom(loadedRoomId);
return;
}
}, [ onCloseClick, loadedRoomId ]);
return (
<NitroCardView className="nitro-mod-tools-chatlog" simple={ true }>
<NitroCardHeaderView headerText={ 'Room Chatlog' + (roomName ? ': ' + roomName : '') } onCloseClick={ event => handleClick('close') } />
<NitroCardContentView className="text-black h-100">
<div className="w-100 d-flex justify-content-end">
<button className="btn btn-sm btn-primary me-2" onClick={ event => handleClick('visit_room') }>Visit Room</button>
<button className="btn btn-sm btn-primary">Room Tools</button>
</div>
<div className="chatlog-messages overflow-auto">
{ messages && <table className="table table-striped">
<thead>
<tr>
<th className="text-center">Time</th>
<th className="text-center">User</th>
<th>Message</th>
</tr>
</thead>
<tbody>
{ messages.map((message, index) =>
{
return <tr key={ index }>
<td className="text-center">{ message.timestamp }</td>
<td className="text-center"><a href="#" className="fw-bold">{ message.userName }</a></td>
<td className="word-break">{ message.message }</td>
</tr>;
}) }
</tbody>
</table> }
</div>
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -0,0 +1,5 @@
export interface ModToolsChatlogViewProps
{
roomId: number;
onCloseClick: () => void;
}

View File

@ -0,0 +1,3 @@
.nitro-mod-tools-room {
width: 240px;
}

View File

@ -0,0 +1,104 @@
import { ModtoolRequestRoomInfoComposer, ModtoolRoomInfoEvent } from 'nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { CreateMessageHook, SendMessageHook } from '../../../../hooks/messages';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { ModToolsRoomViewProps } from './ModToolsRoomView.types';
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);
useEffect(() =>
{
if(infoRequested) return;
SendMessageHook(new ModtoolRequestRoomInfoComposer(roomId));
setInfoRequested(true);
}, [ roomId, infoRequested, setInfoRequested ]);
const onModtoolRoomInfoEvent = useCallback((event: ModtoolRoomInfoEvent) =>
{
const parser = event.getParser();
setLoadedRoomId(parser.id);
setName(parser.name);
setOwnerId(parser.ownerId);
setOwnerName(parser.ownerName);
setOwnerInRoom(parser.ownerInRoom);
setUsersInRoom(parser.playerAmount);
}, [ setLoadedRoomId, setName, setOwnerId, setOwnerName, setOwnerInRoom, setUsersInRoom ]);
CreateMessageHook(ModtoolRoomInfoEvent, onModtoolRoomInfoEvent);
const handleClick = useCallback((action: string, value?: string) =>
{
if(!action) return;
switch(action)
{
case 'close':
onCloseClick();
return;
}
}, [ onCloseClick ]);
return (
<NitroCardView className="nitro-mod-tools-room" simple={ true }>
<NitroCardHeaderView headerText={ 'Room Info' + (name ? ': ' + name : '') } onCloseClick={ event => handleClick('close') } />
<NitroCardContentView className="text-black">
<div className="d-flex justify-content-between align-items-center mb-1">
<div>
<b>Room Owner:</b> <a href="#" className="fw-bold">{ ownerName }</a>
</div>
<button className="btn btn-sm btn-primary">Visit Room</button>
</div>
<div className="d-flex justify-content-between align-items-center mb-1">
<div>
<b>Users in room:</b> { usersInRoom }
</div>
<button className="btn btn-sm btn-primary">Chatlog</button>
</div>
<div className="d-flex justify-content-between align-items-center mb-2">
<div>
<b>Owner in room:</b> { ownerInRoom ? 'Yes' : 'No' }
</div>
<button className="btn btn-sm btn-primary">Edit in HK</button>
</div>
<div className="bg-muted rounded py-1 px-2 mb-2">
<div className="form-check">
<input className="form-check-input" type="checkbox" id="kickUsers" />
<label className="form-check-label" htmlFor="kickUsers">
Kick users out of the room
</label>
</div>
<div className="form-check">
<input className="form-check-input" type="checkbox" id="lockRoom" />
<label className="form-check-label" htmlFor="lockRoom">
Change room lock to doorbell
</label>
</div>
<div className="form-check">
<input className="form-check-input" type="checkbox" id="lockRoom" />
<label className="form-check-label" htmlFor="lockRoom">
Change room name to "Inappro- priate to Hotel Management"
</label>
</div>
</div>
<textarea className="form-control mb-2" placeholder="Type a mandatory message to the users in this text box..."></textarea>
<div className="d-flex justify-content-between">
<button className="btn btn-danger w-100 me-2">Send Caution</button>
<button className="btn btn-success w-100">Send Alert only</button>
</div>
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -0,0 +1,5 @@
export interface ModToolsRoomViewProps
{
roomId: number;
onCloseClick: () => void;
}

View File

@ -0,0 +1,15 @@
import { FC } from 'react';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { ModToolsTicketsViewProps } from './ModToolsTicketsView.types';
export const ModToolsTicketsView: FC<ModToolsTicketsViewProps> = props =>
{
return (
<NitroCardView className="nitro-mod-tools-tickets" simple={ true }>
<NitroCardHeaderView headerText={ "Tickets" } onCloseClick={ event => {} } />
<NitroCardContentView className="text-black">
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -0,0 +1,2 @@
export interface ModToolsTicketsViewProps
{}

View File

@ -0,0 +1,15 @@
import { FC } from 'react';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { ModToolsUserViewProps } from './ModToolsUserView.types';
export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
{
return (
<NitroCardView className="nitro-mod-tools-user" simple={ true }>
<NitroCardHeaderView headerText={ "User Info" } onCloseClick={ event => {} } />
<NitroCardContentView className="text-black">
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -0,0 +1,2 @@
export interface ModToolsUserViewProps
{}

View File

@ -1,8 +1,8 @@
import { Dispose, DropBounce, EaseOut, JumpBy, Motions, NitroToolbarAnimateIconEvent, Queue, UserFigureEvent, Wait } from 'nitro-renderer';
import { UserInfoEvent } from 'nitro-renderer/src/nitro/communication/messages/incoming/user/data/UserInfoEvent';
import { UserInfoDataParser } from 'nitro-renderer/src/nitro/communication/messages/parser/user/data/UserInfoDataParser';
import { Dispose, DropBounce, EaseOut, JumpBy, Motions, NitroToolbarAnimateIconEvent, Queue, UserFigureEvent, UserInfoDataParser, UserInfoEvent, Wait } from 'nitro-renderer';
import { FC, useCallback, useState } from 'react';
import { AvatarEditorEvent, CatalogEvent, FriendListEvent, InventoryEvent, NavigatorEvent, RoomWidgetCameraEvent, UnseenItemTrackerUpdateEvent } from '../../events';
import { AvatarEditorEvent, CatalogEvent, FriendListEvent, InventoryEvent, NavigatorEvent, RoomWidgetCameraEvent } from '../../events';
import { UnseenItemTrackerUpdateEvent } from '../../events/inventory/UnseenItemTrackerUpdateEvent';
import { ModToolsEvent } from '../../events/mod-tools/ModToolsEvent';
import { useRoomEngineEvent } from '../../hooks';
import { dispatchUiEvent, useUiEvent } from '../../hooks/events/ui/ui-event';
import { CreateMessageHook } from '../../hooks/messages/message-event';
@ -114,6 +114,9 @@ export const ToolbarView: FC<ToolbarViewProps> = props =>
dispatchUiEvent(new AvatarEditorEvent(AvatarEditorEvent.TOGGLE_EDITOR));
setMeExpanded(false);
return;
case ToolbarViewItems.MOD_TOOLS_ITEM:
dispatchUiEvent(new ModToolsEvent(ModToolsEvent.TOGGLE_MOD_TOOLS));
return;
}
}, []);
@ -167,6 +170,14 @@ export const ToolbarView: FC<ToolbarViewProps> = props =>
</div>
<div className="d-flex toolbar-right-side">
<div id="toolbar-friend-bar-container" />
<div className="navigation-item" onClick={ event => handleToolbarItemClick(ToolbarViewItems.FRIEND_LIST_ITEM) }>
<i className="icon icon-friendall"></i>
{ (unseenFriendListCount > 0) && (
<div className="position-absolute bg-danger px-1 py-0 rounded shadow count">{ unseenFriendListCount }</div>) }
</div>
<div className="navigation-item" onClick={ event => handleToolbarItemClick(ToolbarViewItems.MOD_TOOLS_ITEM) }>
<i className="icon icon-modtools"></i>
</div>
</div>
</div>
</div>

View File

@ -11,4 +11,5 @@ export class ToolbarViewItems
public static FRIEND_LIST_ITEM: string = 'TVI_FRIEND_LIST_ITEM';
public static CLOTHING_ITEM: string = 'TVI_CLOTHING_ITEM';
public static CAMERA_ITEM: string = 'TVI_CAMERA_ITEM';
public static MOD_TOOLS_ITEM: string = 'TVI_MOD_TOOLS_ITEM';
}