diff --git a/src/api/nitro/room/widgets/handlers/RoomWidgetInfostandHandler.ts b/src/api/nitro/room/widgets/handlers/RoomWidgetInfostandHandler.ts index e86aba4e..be1d8608 100644 --- a/src/api/nitro/room/widgets/handlers/RoomWidgetInfostandHandler.ts +++ b/src/api/nitro/room/widgets/handlers/RoomWidgetInfostandHandler.ts @@ -2,6 +2,7 @@ import { IFurnitureData, NitroEvent, ObjectDataFactory, PetFigureData, PetRespec import { GetNitroInstance, GetRoomEngine, GetSessionDataManager, IsOwnerOfFurniture } from '../../../..'; import { InventoryTradeRequestEvent, WiredSelectObjectEvent } from '../../../../../events'; import { FriendsSendFriendRequestEvent } from '../../../../../events/friends/FriendsSendFriendRequestEvent'; +import { HelpReportUserEvent } from '../../../../../events/help/HelpReportUserEvent'; import { dispatchUiEvent } from '../../../../../hooks/events'; import { SendMessageHook } from '../../../../../hooks/messages'; import { PetSupplementEnum } from '../../../../../views/room/widgets/avatar-info/common/PetSupplementEnum'; @@ -164,6 +165,7 @@ export class RoomWidgetInfostandHandler extends RoomWidgetHandler case RoomWidgetUserActionMessage.REPORT: return; case RoomWidgetUserActionMessage.REPORT_CFH_OTHER: + dispatchUiEvent(new HelpReportUserEvent(userId)); return; case RoomWidgetUserActionMessage.AMBASSADOR_ALERT_USER: this.container.roomSession.sendAmbassadorAlertMessage(userId); diff --git a/src/assets/images/help/help_index.png b/src/assets/images/help/help_index.png new file mode 100644 index 00000000..2844f41e Binary files /dev/null and b/src/assets/images/help/help_index.png differ diff --git a/src/events/chat-history/ChatHistoryEvent.ts b/src/events/chat-history/ChatHistoryEvent.ts new file mode 100644 index 00000000..bf057561 --- /dev/null +++ b/src/events/chat-history/ChatHistoryEvent.ts @@ -0,0 +1,9 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; + +export class ChatHistoryEvent extends NitroEvent +{ + public static SHOW_CHAT_HISTORY: string = 'CHE_SHOW_CHAT_HISTORY'; + public static HIDE_CHAT_HISTORY: string = 'CHE_HIDE_CHAT_HISTORY'; + public static TOGGLE_CHAT_HISTORY: string = 'CHE_TOGGLE_CHAT_HISTORY'; + public static CHAT_HISTORY_CHANGED: string = 'CHE_CHAT_HISTORY_CHANGED'; +} diff --git a/src/events/help/HelpEvent.ts b/src/events/help/HelpEvent.ts new file mode 100644 index 00000000..1403f20f --- /dev/null +++ b/src/events/help/HelpEvent.ts @@ -0,0 +1,8 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; + +export class HelpEvent extends NitroEvent +{ + public static SHOW_HELP_CENTER: string = 'HCE_SHOW_HELP_CENTER'; + public static HIDE_HELP_CENTER: string = 'HCE_HIDE_HELP_CENTER'; + public static TOGGLE_HELP_CENTER: string = 'HCE_TOGGLE_HELP_CENTER'; +} diff --git a/src/events/help/HelpReportUserEvent.ts b/src/events/help/HelpReportUserEvent.ts new file mode 100644 index 00000000..b4711b76 --- /dev/null +++ b/src/events/help/HelpReportUserEvent.ts @@ -0,0 +1,20 @@ +import { HelpEvent } from './HelpEvent'; + +export class HelpReportUserEvent extends HelpEvent +{ + public static REPORT_USER: string = 'HCE_HELP_CENTER_REPORT_USER'; + + private _reportedUserId: number; + + constructor(userId: number) + { + super(HelpReportUserEvent.REPORT_USER); + + this._reportedUserId = userId; + } + + public get reportedUserId(): number + { + return this._reportedUserId; + } +} diff --git a/src/events/mod-tools/ModToolsEvent.ts b/src/events/mod-tools/ModToolsEvent.ts index b6070a36..ac7dc7d8 100644 --- a/src/events/mod-tools/ModToolsEvent.ts +++ b/src/events/mod-tools/ModToolsEvent.ts @@ -5,6 +5,8 @@ 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'; + public static OPEN_ROOM_CHATLOG: string = 'MTE_OPEN_ROOM_CHATLOG'; + public static OPEN_USER_INFO: string = 'MTE_OPEN_USER_INFO'; + public static OPEN_USER_CHATLOG: string = 'MTE_OPEN_USER_CHATLOG'; } diff --git a/src/events/mod-tools/ModToolsOpenRoomChatlogEvent.ts b/src/events/mod-tools/ModToolsOpenRoomChatlogEvent.ts new file mode 100644 index 00000000..692e687d --- /dev/null +++ b/src/events/mod-tools/ModToolsOpenRoomChatlogEvent.ts @@ -0,0 +1,18 @@ +import { ModToolsEvent } from './ModToolsEvent'; + +export class ModToolsOpenRoomChatlogEvent extends ModToolsEvent +{ + private _roomId: number; + + constructor(roomId: number) + { + super(ModToolsEvent.OPEN_ROOM_CHATLOG); + + this._roomId = roomId; + } + + public get roomId(): number + { + return this._roomId; + } +} diff --git a/src/events/mod-tools/ModToolsOpenUserChatlogEvent.ts b/src/events/mod-tools/ModToolsOpenUserChatlogEvent.ts new file mode 100644 index 00000000..aa8858e8 --- /dev/null +++ b/src/events/mod-tools/ModToolsOpenUserChatlogEvent.ts @@ -0,0 +1,18 @@ +import { ModToolsEvent } from './ModToolsEvent'; + +export class ModToolsOpenUserChatlogEvent extends ModToolsEvent +{ + private _userId: number; + + constructor(userId: number) + { + super(ModToolsEvent.OPEN_USER_CHATLOG); + + this._userId = userId; + } + + public get userId(): number + { + return this._userId; + } +} diff --git a/src/events/mod-tools/ModToolsOpenUserInfoEvent.ts b/src/events/mod-tools/ModToolsOpenUserInfoEvent.ts new file mode 100644 index 00000000..58148ee9 --- /dev/null +++ b/src/events/mod-tools/ModToolsOpenUserInfoEvent.ts @@ -0,0 +1,18 @@ +import { ModToolsEvent } from './ModToolsEvent'; + +export class ModToolsOpenUserInfoEvent extends ModToolsEvent +{ + private _userId: number; + + constructor(userId: number) + { + super(ModToolsEvent.OPEN_USER_INFO); + + this._userId = userId; + } + + public get userId(): number + { + return this._userId; + } +} diff --git a/src/events/mod-tools/ModToolsSelectUserEvent.ts b/src/events/mod-tools/ModToolsSelectUserEvent.ts deleted file mode 100644 index 148ffe28..00000000 --- a/src/events/mod-tools/ModToolsSelectUserEvent.ts +++ /dev/null @@ -1,25 +0,0 @@ -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; - } -} diff --git a/src/layout/card/NitroCardView.scss b/src/layout/card/NitroCardView.scss index b604cd44..2ba6ec2e 100644 --- a/src/layout/card/NitroCardView.scss +++ b/src/layout/card/NitroCardView.scss @@ -4,6 +4,17 @@ $nitro-card-tabs-height: 33px; .nitro-card { pointer-events: all; resize: both; + + &.theme-dark { + padding: 2px; + background-color: #1C323F; + border: 2px solid rgba(255, 255, 255, 0.5); + border-radius: 0.25rem; + } + + &.theme-primary { + border: $border-width solid $border-color; + } } .nitro-card-responsive { @@ -15,10 +26,6 @@ $nitro-card-tabs-height: 33px; pointer-events: none; overflow: hidden; - .theme-primary { - border: $border-width solid $border-color; - } - @include media-breakpoint-down(lg) { .draggable-window { diff --git a/src/layout/card/content/NitroCardContentView.scss b/src/layout/card/content/NitroCardContentView.scss index c56cad8c..a3ec273d 100644 --- a/src/layout/card/content/NitroCardContentView.scss +++ b/src/layout/card/content/NitroCardContentView.scss @@ -3,6 +3,10 @@ padding-top: $container-padding-x; padding-bottom: $container-padding-x; overflow: auto; + + &.theme-dark { + background-color: #1C323F !important; + } } @include media-breakpoint-down(lg) { diff --git a/src/layout/card/content/NitroCardContentView.tsx b/src/layout/card/content/NitroCardContentView.tsx index 8565b3ab..4553d7dc 100644 --- a/src/layout/card/content/NitroCardContentView.tsx +++ b/src/layout/card/content/NitroCardContentView.tsx @@ -4,11 +4,11 @@ import { NitroCardContentViewProps } from './NitroCardContextView.types'; export const NitroCardContentView: FC = props => { - const { children = null, className = '', ...rest } = props; + const { theme = 'primary', children = null, className = '', ...rest } = props; const { simple = false } = useNitroCardContext(); return ( -
+
{ children }
); diff --git a/src/layout/card/content/NitroCardContextView.types.ts b/src/layout/card/content/NitroCardContextView.types.ts index 120cd9d3..21b1d68d 100644 --- a/src/layout/card/content/NitroCardContextView.types.ts +++ b/src/layout/card/content/NitroCardContextView.types.ts @@ -2,4 +2,6 @@ import { DetailsHTMLAttributes } from 'react'; export interface NitroCardContentViewProps extends DetailsHTMLAttributes -{} +{ + theme?: string; +} diff --git a/src/layout/card/header/NitroCardHeaderView.scss b/src/layout/card/header/NitroCardHeaderView.scss index b81b748e..86b81a48 100644 --- a/src/layout/card/header/NitroCardHeaderView.scss +++ b/src/layout/card/header/NitroCardHeaderView.scss @@ -15,6 +15,11 @@ } } + &.theme-dark { + background-color: #3d5f6e !important; + color: #fff; + } + .bg-tertiary-split { position: relative; border-bottom: 2px solid darken($quaternary, 5); diff --git a/src/layout/card/header/NitroCardHeaderView.tsx b/src/layout/card/header/NitroCardHeaderView.tsx index 2f314be7..c87585f1 100644 --- a/src/layout/card/header/NitroCardHeaderView.tsx +++ b/src/layout/card/header/NitroCardHeaderView.tsx @@ -4,7 +4,7 @@ import { NitroCardHeaderViewProps } from './NitroCardHeaderView.types'; export const NitroCardHeaderView: FC = props => { - const { headerText = null, onCloseClick = null } = props; + const { headerText = null, onCloseClick = null, theme= 'primary' } = props; const { simple = false } = useNitroCardContext(); const onMouseDown = useCallback((event: MouseEvent) => @@ -31,7 +31,7 @@ export const NitroCardHeaderView: FC = props => return (
-
+
{ headerText }
diff --git a/src/layout/card/header/NitroCardHeaderView.types.ts b/src/layout/card/header/NitroCardHeaderView.types.ts index d1d78d29..7a5c7e88 100644 --- a/src/layout/card/header/NitroCardHeaderView.types.ts +++ b/src/layout/card/header/NitroCardHeaderView.types.ts @@ -3,5 +3,6 @@ import { MouseEvent } from 'react'; export interface NitroCardHeaderViewProps { headerText: string; + theme?: string; onCloseClick: (event: MouseEvent) => void; } diff --git a/src/views/Styles.scss b/src/views/Styles.scss index 4f31d9e5..77dc121f 100644 --- a/src/views/Styles.scss +++ b/src/views/Styles.scss @@ -20,3 +20,5 @@ @import './achievements/AchievementsView'; @import './user-settings/UserSettingsView'; @import './user-profile/UserProfileVew'; +@import './chat-history/ChatHistoryView'; +@import './help/HelpView'; diff --git a/src/views/chat-history/ChatHistoryMessageHandler.tsx b/src/views/chat-history/ChatHistoryMessageHandler.tsx new file mode 100644 index 00000000..d1165d39 --- /dev/null +++ b/src/views/chat-history/ChatHistoryMessageHandler.tsx @@ -0,0 +1,107 @@ +import { RoomInfoEvent, RoomSessionChatEvent, RoomSessionEvent } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useState } from 'react'; +import { GetRoomSession } from '../../api'; +import { CreateMessageHook, useRoomSessionManagerEvent } from '../../hooks'; +import { useChatHistoryContext } from './context/ChatHistoryContext'; +import { ChatEntryType, CHAT_HISTORY_MAX, IChatEntry, IRoomHistoryEntry, ROOM_HISTORY_MAX } from './context/ChatHistoryContext.types'; +import { currentDate } from './utils/Utilities'; + +export const ChatHistoryMessageHandler: FC<{}> = props => +{ + const { chatHistoryState = null, roomHistoryState = null } = useChatHistoryContext(); + + const [needsRoomInsert, setNeedsRoomInsert ] = useState(false); + + const addChatEntry = useCallback((entry: IChatEntry) => + { + entry.id = chatHistoryState.chats.length; + + chatHistoryState.chats.push(entry); + + //check for overflow + if(chatHistoryState.chats.length > CHAT_HISTORY_MAX) + { + chatHistoryState.chats.shift(); + } + chatHistoryState.notify(); + + //dispatchUiEvent(new ChatHistoryEvent(ChatHistoryEvent.CHAT_HISTORY_CHANGED)); + + }, [chatHistoryState]); + + const addRoomHistoryEntry = useCallback((entry: IRoomHistoryEntry) => + { + roomHistoryState.roomHistory.push(entry); + + // check for overflow + if(roomHistoryState.roomHistory.length > ROOM_HISTORY_MAX) + { + roomHistoryState.roomHistory.shift(); + } + + roomHistoryState.notify(); + }, [roomHistoryState]); + + const onChatEvent = useCallback((event: RoomSessionChatEvent) => + { + const roomSession = GetRoomSession(); + + if(!roomSession) return; + + const userData = roomSession.userDataManager.getUserDataByIndex(event.objectId); + + if(!userData) return; + + const timeString = currentDate(); + + const entry: IChatEntry = { id: -1, entityId: userData.webID, name: userData.name, look: userData.figure, entityType: userData.type, message: event.message, timestamp: timeString, type: ChatEntryType.TYPE_CHAT, roomId: roomSession.roomId }; + + addChatEntry(entry); + }, [addChatEntry]); + + useRoomSessionManagerEvent(RoomSessionChatEvent.CHAT_EVENT, onChatEvent); + + const onRoomSessionEvent = useCallback((event: RoomSessionEvent) => + { + switch(event.type) + { + case RoomSessionEvent.STARTED: + setNeedsRoomInsert(true); + break; + case RoomSessionEvent.ENDED: + //dispatchUiEvent(new ChatHistoryEvent(ChatHistoryEvent.HIDE_CHAT_HISTORY)); + break; + } + }, []); + + useRoomSessionManagerEvent(RoomSessionEvent.ENDED, onRoomSessionEvent); + useRoomSessionManagerEvent(RoomSessionEvent.STARTED, onRoomSessionEvent); + + const onRoomInfoEvent = useCallback((event: RoomInfoEvent) => + { + const parser = event.getParser(); + + if(!parser) return; + + const session = GetRoomSession(); + + if(!session || (session.roomId !== parser.data.roomId)) return; + + if(needsRoomInsert) + { + const chatEntry: IChatEntry = { id: -1, entityId: parser.data.roomId, name: parser.data.roomName, timestamp: currentDate(), type: ChatEntryType.TYPE_ROOM_INFO, roomId: parser.data.roomId }; + + addChatEntry(chatEntry); + + const roomEntry: IRoomHistoryEntry = { id: parser.data.roomId, name: parser.data.roomName }; + + addRoomHistoryEntry(roomEntry); + + setNeedsRoomInsert(false); + } + }, [addChatEntry, addRoomHistoryEntry, needsRoomInsert]); + + CreateMessageHook(RoomInfoEvent, onRoomInfoEvent); + + return null; +} diff --git a/src/views/chat-history/ChatHistoryView.scss b/src/views/chat-history/ChatHistoryView.scss new file mode 100644 index 00000000..d103ad8c --- /dev/null +++ b/src/views/chat-history/ChatHistoryView.scss @@ -0,0 +1,27 @@ +.nitro-chat-history +{ + width: 300px; + + .chat-history-content + { + + .chat-history-container + { + min-height: 200px; + + .chat-history-list + { + .chathistory-entry + { + .light { + background-color: #121f27; + } + + .dark { + background-color: #0d171d; + } + } + } + } + } +} diff --git a/src/views/chat-history/ChatHistoryView.tsx b/src/views/chat-history/ChatHistoryView.tsx new file mode 100644 index 00000000..2a29cdd8 --- /dev/null +++ b/src/views/chat-history/ChatHistoryView.tsx @@ -0,0 +1,171 @@ +import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { AutoSizer, CellMeasurer, CellMeasurerCache, List, ListRowProps, ListRowRenderer, Size } from 'react-virtualized'; +import { RenderedRows } from 'react-virtualized/dist/es/List'; +import { ChatHistoryEvent } from '../../events/chat-history/ChatHistoryEvent'; +import { useUiEvent } from '../../hooks'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../layout'; +import { ChatHistoryMessageHandler } from './ChatHistoryMessageHandler'; +import { ChatHistoryState } from './common/ChatHistoryState'; +import { SetChatHistory } from './common/GetChatHistory'; +import { RoomHistoryState } from './common/RoomHistoryState'; +import { ChatHistoryContextProvider } from './context/ChatHistoryContext'; +import { ChatEntryType } from './context/ChatHistoryContext.types'; + + + +export const ChatHistoryView: FC<{}> = props => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ needsScroll, setNeedsScroll ] = useState(false); + const [ chatHistoryUpdateId, setChatHistoryUpdateId ] = useState(-1); + const [ roomHistoryUpdateId, setRoomHistoryUpdateId ] = useState(-1); + const [ chatHistoryState, setChatHistoryState ] = useState(new ChatHistoryState()); + const [ roomHistoryState, setRoomHistoryState ] = useState(new RoomHistoryState()); + const elementRef = useRef(null); + + useEffect(() => + { + const chatState = new ChatHistoryState(); + const roomState = new RoomHistoryState(); + + SetChatHistory(chatState); + + chatState.notifier = () => setChatHistoryUpdateId(prevValue => (prevValue + 1)); + roomState.notifier = () => setRoomHistoryUpdateId(prevValue => (prevValue + 1)); + + setChatHistoryState(chatState); + setRoomHistoryState(roomState); + + return () => {chatState.notifier = null; roomState.notifier = null;}; + }, []); + + const onChatHistoryEvent = useCallback((event: ChatHistoryEvent) => + { + switch(event.type) + { + case ChatHistoryEvent.SHOW_CHAT_HISTORY: + setIsVisible(true); + break; + case ChatHistoryEvent.HIDE_CHAT_HISTORY: + setIsVisible(false); + break; + case ChatHistoryEvent.TOGGLE_CHAT_HISTORY: + setIsVisible(!isVisible); + break; + case ChatHistoryEvent.CHAT_HISTORY_CHANGED: + break; + } + }, [isVisible]); + + useUiEvent(ChatHistoryEvent.HIDE_CHAT_HISTORY, onChatHistoryEvent); + useUiEvent(ChatHistoryEvent.SHOW_CHAT_HISTORY, onChatHistoryEvent); + useUiEvent(ChatHistoryEvent.TOGGLE_CHAT_HISTORY, onChatHistoryEvent); + useUiEvent(ChatHistoryEvent.CHAT_HISTORY_CHANGED, onChatHistoryEvent); + + const cache = useMemo(() => +{ + return new CellMeasurerCache({ + defaultHeight: 25, + fixedWidth: true, + //keyMapper: (index) => chatHistoryState.chats[index].id + }); + }, []); + + const RowRenderer: ListRowRenderer = (props: ListRowProps) => + { + const item = chatHistoryState.chats[props.index]; + + const isDark = (props.index % 2 === 0); + + return ( + +
+ {(item.type === ChatEntryType.TYPE_CHAT) && +
+
{item.timestamp}
+
+
{item.message}
+
+ } + {(item.type === ChatEntryType.TYPE_ROOM_INFO) && +
+
{item.timestamp}
+
{item.name}
+
+ } + +
+ + ); + }; + + const onResize = useCallback((info: Size) => + { + cache.clearAll(); + }, [cache]); + + const onRowsRendered = useCallback((info: RenderedRows) => + { + if(elementRef && elementRef.current && isVisible && needsScroll) + { + console.log('stop ' + info.stopIndex); + //if(chatHistoryState.chats.length > 0) elementRef.current.measureAllRows(); + elementRef.current.scrollToRow(chatHistoryState.chats.length); + console.log('scroll') + setNeedsScroll(false); + } + }, [chatHistoryState.chats.length, isVisible, needsScroll]); + + useEffect(() => + { + + if(elementRef && elementRef.current && isVisible) + { + //if(chatHistoryState.chats.length > 0) elementRef.current.measureAllRows(); + elementRef.current.scrollToRow(chatHistoryState.chats.length); + } + //console.log(chatHistoryState.chats.length); + + setNeedsScroll(true); + }, [chatHistoryState.chats, isVisible, chatHistoryUpdateId]); + + return ( + + + {isVisible && + + setIsVisible(false) } theme={'dark'}/> + +
+ + {({ height, width }) => + { + return ( + + ) + } + } + +
+
+
+ } +
+ ); +} diff --git a/src/views/chat-history/common/ChatHistoryState.ts b/src/views/chat-history/common/ChatHistoryState.ts new file mode 100644 index 00000000..895c00a1 --- /dev/null +++ b/src/views/chat-history/common/ChatHistoryState.ts @@ -0,0 +1,32 @@ +import { IChatEntry, IChatHistoryState } from '../context/ChatHistoryContext.types'; + +export class ChatHistoryState implements IChatHistoryState +{ + private _chats: IChatEntry[]; + private _notifier: () => void; + + constructor() + { + this._chats = []; + } + + public get chats(): IChatEntry[] + { + return this._chats; + } + + public get notifier(): () => void + { + return this._notifier; + } + + public set notifier(notifier: () => void) + { + this._notifier = notifier; + } + + notify(): void + { + if(this._notifier) this._notifier(); + } +} diff --git a/src/views/chat-history/common/GetChatHistory.ts b/src/views/chat-history/common/GetChatHistory.ts new file mode 100644 index 00000000..d39ef584 --- /dev/null +++ b/src/views/chat-history/common/GetChatHistory.ts @@ -0,0 +1,7 @@ +import { IChatHistoryState } from '../context/ChatHistoryContext.types'; + +let GLOBAL_CHATS: IChatHistoryState = null; + +export const SetChatHistory = (chatHistory: IChatHistoryState) => (GLOBAL_CHATS = chatHistory); + +export const GetChatHistory = () => GLOBAL_CHATS; diff --git a/src/views/chat-history/common/RoomHistoryState.ts b/src/views/chat-history/common/RoomHistoryState.ts new file mode 100644 index 00000000..d3002b81 --- /dev/null +++ b/src/views/chat-history/common/RoomHistoryState.ts @@ -0,0 +1,32 @@ +import { IRoomHistoryEntry, IRoomHistoryState } from '../context/ChatHistoryContext.types'; + +export class RoomHistoryState implements IRoomHistoryState +{ + private _roomHistory: IRoomHistoryEntry[]; + private _notifier: () => void; + + constructor() + { + this._roomHistory = []; + } + + public get roomHistory(): IRoomHistoryEntry[] + { + return this._roomHistory; + } + + public get notifier(): () => void + { + return this._notifier; + } + + public set notifier(notifier: () => void) + { + this._notifier = notifier; + } + + notify(): void + { + if(this._notifier) this._notifier(); + } +} diff --git a/src/views/chat-history/context/ChatHistoryContext.tsx b/src/views/chat-history/context/ChatHistoryContext.tsx new file mode 100644 index 00000000..981d6007 --- /dev/null +++ b/src/views/chat-history/context/ChatHistoryContext.tsx @@ -0,0 +1,14 @@ +import { createContext, FC, useContext } from 'react'; +import { ChatHistoryContextProps, IChatHistoryContext } from './ChatHistoryContext.types'; + +const ChatHistoryContext = createContext({ + chatHistoryState: null, + roomHistoryState: null +}); + +export const ChatHistoryContextProvider: FC = props => +{ + return { props.children } +} + +export const useChatHistoryContext = () => useContext(ChatHistoryContext); diff --git a/src/views/chat-history/context/ChatHistoryContext.types.ts b/src/views/chat-history/context/ChatHistoryContext.types.ts new file mode 100644 index 00000000..51cd19cf --- /dev/null +++ b/src/views/chat-history/context/ChatHistoryContext.types.ts @@ -0,0 +1,50 @@ +import { ProviderProps } from 'react'; + +export interface IChatHistoryContext +{ + chatHistoryState: IChatHistoryState; + roomHistoryState: IRoomHistoryState; +} + +export interface IChatHistoryState { + chats: IChatEntry[]; + notifier: () => void + notify(): void; +} + +export interface IRoomHistoryState { + roomHistory: IRoomHistoryEntry[]; + notifier: () => void + notify(): void; +} + +export interface IChatEntry { + id: number; + entityId: number; + name: string; + look?: string; + message?: string; + entityType?: number; + roomId: number; + timestamp: string; + type: number; +} + +export interface IRoomHistoryEntry { + id: number; + name: string; +} + +export class ChatEntryType +{ + public static TYPE_CHAT = 1; + public static TYPE_ROOM_INFO = 2; +} + +export const CHAT_HISTORY_MAX = 1000; +export const ROOM_HISTORY_MAX = 10; + +export interface ChatHistoryContextProps extends ProviderProps +{ + +} diff --git a/src/views/chat-history/utils/Utilities.ts b/src/views/chat-history/utils/Utilities.ts new file mode 100644 index 00000000..29b00cfa --- /dev/null +++ b/src/views/chat-history/utils/Utilities.ts @@ -0,0 +1,5 @@ +export const currentDate = () => +{ + const currentTime = new Date(); + return `${currentTime.getHours().toString().padStart(2, '0')}:${currentTime.getMinutes().toString().padStart(2, '0')}`; +} diff --git a/src/views/help/HelpMessageHandler.tsx b/src/views/help/HelpMessageHandler.tsx index f71634b7..959abdb9 100644 --- a/src/views/help/HelpMessageHandler.tsx +++ b/src/views/help/HelpMessageHandler.tsx @@ -1,12 +1,10 @@ -import { CallForHelpResultMessageEvent, FurnitureListItemParser, PetData } from '@nitrots/nitro-renderer'; +import { CallForHelpResultMessageEvent } from '@nitrots/nitro-renderer'; import { FC, useCallback } from 'react'; import { LocalizeText } from '../../api'; import { CreateMessageHook } from '../../hooks/messages/message-event'; import { NotificationAlertType } from '../notification-center/common/NotificationAlertType'; import { NotificationUtilities } from '../notification-center/common/NotificationUtilities'; import { GetCloseReasonKey } from './common/GetCloseReasonKey'; -let furniMsgFragments: Map[] = null; -let petMsgFragments: Map[] = null; export const HelpMessageHandler: FC<{}> = props => { diff --git a/src/views/help/HelpView.scss b/src/views/help/HelpView.scss new file mode 100644 index 00000000..f7f513f8 --- /dev/null +++ b/src/views/help/HelpView.scss @@ -0,0 +1,10 @@ +.nitro-help { + height: 430px; + width: 300px; + + .index-image { + background: url('../../assets/images/help/help_index.png'); + width: 126px; + height: 105px; + } +} diff --git a/src/views/help/HelpView.tsx b/src/views/help/HelpView.tsx index 4917c713..630d41d1 100644 --- a/src/views/help/HelpView.tsx +++ b/src/views/help/HelpView.tsx @@ -1,11 +1,79 @@ -import { FC } from 'react'; +import { FC, useCallback, useState } from 'react'; +import { LocalizeText } from '../../api'; +import { HelpEvent } from '../../events/help/HelpEvent'; +import { HelpReportUserEvent } from '../../events/help/HelpReportUserEvent'; +import { useUiEvent } from '../../hooks'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../layout'; +import { HelpContextProvider } from './context/HelpContext'; +import { IHelpReportState, initialReportState } from './context/HelpContext.types'; import { HelpMessageHandler } from './HelpMessageHandler'; +import { DescribeReportView } from './views/DescribeReportView'; +import { HelpIndexView } from './views/HelpIndexView'; +import { SelectReportedChatsView } from './views/SelectReportedChatsView'; +import { SelectReportedUserView } from './views/SelectReportedUserView'; +import { SelectTopicView } from './views/SelectTopicView'; export const HelpView: FC<{}> = props => { + const [isVisible, setIsVisible] = useState(false); + const [ helpReportState, setHelpReportState ] = useState(initialReportState); + + const onHelpEvent = useCallback((event: HelpEvent) => + { + setHelpReportState(initialReportState); + + switch(event.type) + { + case HelpEvent.SHOW_HELP_CENTER: + setIsVisible(true); + break; + case HelpEvent.HIDE_HELP_CENTER: + setIsVisible(false); + break; + case HelpEvent.TOGGLE_HELP_CENTER: + setIsVisible(!isVisible); + break; + case HelpReportUserEvent.REPORT_USER: + const reportEvent = event as HelpReportUserEvent; + const reportState = Object.assign({}, helpReportState ); + reportState.reportedUserId = reportEvent.reportedUserId; + reportState.currentStep = 2; + setHelpReportState(reportState); + setIsVisible(true); + break; + } + }, [helpReportState, isVisible]); + + useUiEvent(HelpEvent.TOGGLE_HELP_CENTER, onHelpEvent); + useUiEvent(HelpEvent.SHOW_HELP_CENTER, onHelpEvent); + useUiEvent(HelpEvent.HIDE_HELP_CENTER, onHelpEvent); + useUiEvent(HelpReportUserEvent.REPORT_USER, onHelpEvent); + + const CurrentStepView = useCallback(() => + { + switch(helpReportState.currentStep) + { + case 0: return + case 1: return + case 2: return + case 3: return + case 4: return + } + + return null; + }, [helpReportState.currentStep]); + return ( - <> + - + {isVisible && + + setIsVisible(false)} /> + + + + + } + ); } diff --git a/src/views/help/common/IUser.ts b/src/views/help/common/IUser.ts new file mode 100644 index 00000000..bd3852ab --- /dev/null +++ b/src/views/help/common/IUser.ts @@ -0,0 +1,5 @@ +export interface IUser +{ + id: number; + username: string; +} diff --git a/src/views/help/context/HelpContext.tsx b/src/views/help/context/HelpContext.tsx new file mode 100644 index 00000000..2cad4a52 --- /dev/null +++ b/src/views/help/context/HelpContext.tsx @@ -0,0 +1,14 @@ +import { createContext, FC, useContext } from 'react'; +import { HelpContextProps, IHelpContext } from './HelpContext.types'; + +const HelpContext = createContext({ + helpReportState: null, + setHelpReportState: null +}); + +export const HelpContextProvider: FC = props => +{ + return { props.children } +} + +export const useHelpContext = () => useContext(HelpContext); diff --git a/src/views/help/context/HelpContext.types.ts b/src/views/help/context/HelpContext.types.ts new file mode 100644 index 00000000..9368308b --- /dev/null +++ b/src/views/help/context/HelpContext.types.ts @@ -0,0 +1,33 @@ +import { ProviderProps } from 'react'; +import { IChatEntry } from '../../chat-history/context/ChatHistoryContext.types'; + +export interface IHelpContext +{ + helpReportState: IHelpReportState; + setHelpReportState: React.Dispatch>; +} + +export interface IHelpReportState { + reportedUserId: number; + reportedChats: IChatEntry[]; + cfhCategory: number; + cfhTopic: number; + roomId: number; + message: string; + currentStep: number; +} + +export const initialReportState: IHelpReportState = { + reportedUserId: -1, + reportedChats: [], + cfhCategory: -1, + cfhTopic: -1, + roomId: -1, + message: '', + currentStep: 0 +} + +export interface HelpContextProps extends ProviderProps +{ + +} diff --git a/src/views/help/views/DescribeReportView.tsx b/src/views/help/views/DescribeReportView.tsx new file mode 100644 index 00000000..f936d225 --- /dev/null +++ b/src/views/help/views/DescribeReportView.tsx @@ -0,0 +1,50 @@ +import { CallForHelpMessageComposer } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useState } from 'react'; +import { LocalizeText } from '../../../api'; +import { HelpEvent } from '../../../events/help/HelpEvent'; +import { dispatchUiEvent, SendMessageHook } from '../../../hooks'; +import { useHelpContext } from '../context/HelpContext'; + +export const DescribeReportView: FC<{}> = props => +{ + const { helpReportState = null, setHelpReportState = null } = useHelpContext(); + const [message, setMessage] = useState(''); + + const submitReport = useCallback(() => + { + if(message.length < 15) return; + + const reportState = Object.assign({}, helpReportState); + + reportState.message = message; + + setHelpReportState(reportState); + + const roomId = reportState.reportedChats[0].roomId; + const chats: (string | number )[] = []; + reportState.reportedChats.forEach(entry => + { + chats.push(entry.entityId); + chats.push(entry.message); + }); + + SendMessageHook(new CallForHelpMessageComposer(message, reportState.cfhTopic, reportState.reportedUserId, roomId, chats)); + + dispatchUiEvent(new HelpEvent(HelpEvent.HIDE_HELP_CENTER)); + }, [helpReportState, message, setHelpReportState]); + + return ( + <> +
+

{LocalizeText('help.emergency.chat_report.subtitle')}

+
{LocalizeText('help.cfh.input.text')}
+
+ +
+ +
- - + +
diff --git a/src/views/mod-tools/views/room/ModToolsRoomView.types.ts b/src/views/mod-tools/views/room/room-tools/ModToolsRoomView.types.ts similarity index 100% rename from src/views/mod-tools/views/room/ModToolsRoomView.types.ts rename to src/views/mod-tools/views/room/room-tools/ModToolsRoomView.types.ts diff --git a/src/views/mod-tools/views/tickets/ModToolsTicketView.scss b/src/views/mod-tools/views/tickets/ModToolsTicketView.scss new file mode 100644 index 00000000..f2459483 --- /dev/null +++ b/src/views/mod-tools/views/tickets/ModToolsTicketView.scss @@ -0,0 +1,11 @@ +.nitro-mod-tools-tickets +{ + width: 400px; + height: 200px; +} + +.nitro-mod-tools-handle-issue +{ + width: 400px; + height: 300px; +} diff --git a/src/views/mod-tools/views/tickets/ModToolsTicketsView.tsx b/src/views/mod-tools/views/tickets/ModToolsTicketsView.tsx index 5d2ebf47..e2ccea90 100644 --- a/src/views/mod-tools/views/tickets/ModToolsTicketsView.tsx +++ b/src/views/mod-tools/views/tickets/ModToolsTicketsView.tsx @@ -1,6 +1,13 @@ -import { FC, useState } from 'react'; +import { IssueMessageData } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useMemo, useState } from 'react'; +import { GetSessionDataManager } from '../../../../api'; import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../layout'; +import { useModToolsContext } from '../../context/ModToolsContext'; +import { IssueInfoView } from './issue-info/IssueInfoView'; import { ModToolsTicketsViewProps } from './ModToolsTicketsView.types'; +import { ModToolsMyIssuesTabView } from './my-issues/ModToolsMyIssuesTabView'; +import { ModToolsOpenIssuesTabView } from './open-issues/ModToolsOpenIssuesTabView'; +import { ModToolsPickedIssuesTabView } from './picked-issues/ModToolsPickedIssuesTabView'; const TABS: string[] = [ 'Open Issues', @@ -11,10 +18,70 @@ const TABS: string[] = [ export const ModToolsTicketsView: FC = props => { const { onCloseClick = null } = props; - + const { modToolsState = null } = useModToolsContext(); + const { tickets= null } = modToolsState; const [ currentTab, setCurrentTab ] = useState(0); + const [ issueInfoWindows, setIssueInfoWindows ] = useState([]); + + const openIssues = useMemo(() => + { + if(!tickets) return []; + + return tickets.filter(issue => issue.state === IssueMessageData.STATE_OPEN); + }, [tickets]); + + const myIssues = useMemo(() => + { + if(!tickets) return []; + + return tickets.filter(issue => (issue.state === IssueMessageData.STATE_PICKED) && (issue.pickerUserId === GetSessionDataManager().userId)); + }, [tickets]); + + const pickedIssues = useMemo(() => + { + if(!tickets) return []; + + return tickets.filter(issue => issue.state === IssueMessageData.STATE_PICKED); + }, [tickets]); + + const onIssueInfoClosed = useCallback((issueId: number) => + { + const indexOfValue = issueInfoWindows.indexOf(issueId); + + if(indexOfValue === -1) return; + + const newValues = Array.from(issueInfoWindows); + newValues.splice(indexOfValue, 1); + setIssueInfoWindows(newValues); + }, [issueInfoWindows]); + + const onIssueHandleClicked = useCallback((issueId: number) => + { + if(issueInfoWindows.indexOf(issueId) === -1) + { + const newValues = Array.from(issueInfoWindows); + newValues.push(issueId); + setIssueInfoWindows(newValues); + } + else + { + onIssueInfoClosed(issueId); + } + }, [issueInfoWindows, onIssueInfoClosed]); + + const CurrentTabComponent = useCallback(() => + { + switch(currentTab) + { + case 0: return ; + case 1: return ; + case 2: return ; + default: return null; + } + }, [currentTab, myIssues, onIssueHandleClicked, openIssues, pickedIssues]); return ( + <> @@ -26,8 +93,14 @@ export const ModToolsTicketsView: FC = props => ); }) } -
+
+ +
+ { + issueInfoWindows && issueInfoWindows.map(issueId => ) + } + ); } diff --git a/src/views/mod-tools/views/tickets/issue-info/CfhChatlogView.tsx b/src/views/mod-tools/views/tickets/issue-info/CfhChatlogView.tsx new file mode 100644 index 00000000..fdb4c28e --- /dev/null +++ b/src/views/mod-tools/views/tickets/issue-info/CfhChatlogView.tsx @@ -0,0 +1,37 @@ +import { CfhChatlogData, CfhChatlogEvent, GetCfhChatlogMessageComposer } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useState } from 'react'; +import { CreateMessageHook, SendMessageHook } from '../../../../../hooks'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout'; +import { ChatlogView } from '../../chatlog/ChatlogView'; +import { CfhChatlogViewProps } from './CfhChatlogView.types'; + +export const CfhChatlogView: FC = props => +{ + const { onCloseClick = null, issueId = null } = props; + const [ chatlogData, setChatlogData ] = useState(null); + + useEffect(() => + { + SendMessageHook(new GetCfhChatlogMessageComposer(issueId)); + }, [issueId]); + + const onCfhChatlogEvent = useCallback((event: CfhChatlogEvent) => + { + const parser = event.getParser(); + + if(!parser || parser.data.issueId !== issueId) return; + + setChatlogData(parser.data); + }, [issueId]); + + CreateMessageHook(CfhChatlogEvent, onCfhChatlogEvent); + + return ( + + + + { chatlogData && } + + + ); +} diff --git a/src/views/mod-tools/views/tickets/issue-info/CfhChatlogView.types.ts b/src/views/mod-tools/views/tickets/issue-info/CfhChatlogView.types.ts new file mode 100644 index 00000000..c40c18ad --- /dev/null +++ b/src/views/mod-tools/views/tickets/issue-info/CfhChatlogView.types.ts @@ -0,0 +1,5 @@ +export interface CfhChatlogViewProps +{ + issueId: number; + onCloseClick(): void; +} diff --git a/src/views/mod-tools/views/tickets/issue-info/IssueInfoView.tsx b/src/views/mod-tools/views/tickets/issue-info/IssueInfoView.tsx new file mode 100644 index 00000000..61a9784a --- /dev/null +++ b/src/views/mod-tools/views/tickets/issue-info/IssueInfoView.tsx @@ -0,0 +1,72 @@ +import { CloseIssuesMessageComposer, ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useMemo, useState } from 'react'; +import { LocalizeText } from '../../../../../api'; +import { ModToolsOpenUserInfoEvent } from '../../../../../events/mod-tools/ModToolsOpenUserInfoEvent'; +import { dispatchUiEvent, SendMessageHook } from '../../../../../hooks'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout'; +import { getSourceName } from '../../../common/IssueCategoryNames'; +import { useModToolsContext } from '../../../context/ModToolsContext'; +import { CfhChatlogView } from './CfhChatlogView'; +import { IssueInfoViewProps } from './IssueInfoView.types'; + +export const IssueInfoView: FC = props => +{ + const { issueId = null, onIssueInfoClosed = null } = props; + const { modToolsState = null } = useModToolsContext(); + const { tickets= null } = modToolsState; + const [ cfhChatlogOpen, setcfhChatlogOpen ] = useState(false); + + const ticket = useMemo(() => + { + return tickets.find( issue => issue.issueId === issueId); + }, [issueId, tickets]); + + const onReleaseIssue = useCallback((issueId: number) => + { + SendMessageHook(new ReleaseIssuesMessageComposer([issueId])); + onIssueInfoClosed(issueId); + }, [onIssueInfoClosed]); + + const openUserInfo = useCallback((userId: number) => + { + dispatchUiEvent(new ModToolsOpenUserInfoEvent(userId)); + }, []); + + const closeIssue = useCallback((resolutionType: number) => + { + SendMessageHook(new CloseIssuesMessageComposer([issueId], resolutionType)); + onIssueInfoClosed(issueId) + }, [issueId, onIssueInfoClosed]); + + return ( + <> + + onIssueInfoClosed(issueId)} /> + +
+
+

Issue Information

+
Source: {getSourceName(ticket.categoryId)}
+
Category: {LocalizeText('help.cfh.topic.' + ticket.reportedCategoryId)}
+
Description: {ticket.message}
+
Caller:
+
Reported User:
+
+
+
+ +
+
+ + + + +
+
+
+
+
+ { cfhChatlogOpen && setcfhChatlogOpen(false) }/>} + + ); +} diff --git a/src/views/mod-tools/views/tickets/issue-info/IssueInfoView.types.ts b/src/views/mod-tools/views/tickets/issue-info/IssueInfoView.types.ts new file mode 100644 index 00000000..1b28d803 --- /dev/null +++ b/src/views/mod-tools/views/tickets/issue-info/IssueInfoView.types.ts @@ -0,0 +1,5 @@ +export interface IssueInfoViewProps +{ + issueId: number; + onIssueInfoClosed(issueId: number): void; +} diff --git a/src/views/mod-tools/views/tickets/my-issues/ModToolsMyIssuesTabView.tsx b/src/views/mod-tools/views/tickets/my-issues/ModToolsMyIssuesTabView.tsx new file mode 100644 index 00000000..1a3fcde3 --- /dev/null +++ b/src/views/mod-tools/views/tickets/my-issues/ModToolsMyIssuesTabView.tsx @@ -0,0 +1,45 @@ +import { ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer'; +import { FC, useCallback } from 'react'; +import { SendMessageHook } from '../../../../../hooks'; +import { ModToolsMyIssuesTabViewProps } from './ModToolsMyIssuesTabView.types'; + +export const ModToolsMyIssuesTabView: FC = props => +{ + const { myIssues = null, onIssueHandleClick = null } = props; + + + const onReleaseIssue = useCallback((issueId: number) => + { + SendMessageHook(new ReleaseIssuesMessageComposer([issueId])); + }, []); + + return ( + <> + + + + + + + + + + + + {myIssues.map(issue => + { + return ( + + + + + + + ) + }) + } + +
TypeRoom/PlayerOpened
{issue.categoryId}{issue.reportedUserName}{new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString()}
+ + ); +} diff --git a/src/views/mod-tools/views/tickets/my-issues/ModToolsMyIssuesTabView.types.ts b/src/views/mod-tools/views/tickets/my-issues/ModToolsMyIssuesTabView.types.ts new file mode 100644 index 00000000..6d03fbf8 --- /dev/null +++ b/src/views/mod-tools/views/tickets/my-issues/ModToolsMyIssuesTabView.types.ts @@ -0,0 +1,7 @@ +import { IssueMessageData } from '@nitrots/nitro-renderer'; + +export interface ModToolsMyIssuesTabViewProps +{ + myIssues: IssueMessageData[]; + onIssueHandleClick(issueId: number): void; +} diff --git a/src/views/mod-tools/views/tickets/open-issues/ModToolsOpenIssuesTabView.tsx b/src/views/mod-tools/views/tickets/open-issues/ModToolsOpenIssuesTabView.tsx new file mode 100644 index 00000000..4f8bb6d6 --- /dev/null +++ b/src/views/mod-tools/views/tickets/open-issues/ModToolsOpenIssuesTabView.tsx @@ -0,0 +1,42 @@ +import { PickIssuesMessageComposer } from '@nitrots/nitro-renderer'; +import { FC, useCallback } from 'react'; +import { SendMessageHook } from '../../../../../hooks'; +import { ModToolsOpenIssuesTabViewProps } from './ModToolsOpenIssuesTabView.types'; + +export const ModToolsOpenIssuesTabView: FC = props => +{ + const { openIssues = null } = props; + + const onPickIssue = useCallback((issueId: number) => + { + SendMessageHook(new PickIssuesMessageComposer([issueId], false, 0, 'pick issue button')); + }, []); + + return ( + <> + + + + + + + + + + + {openIssues.map(issue => + { + return ( + + + + + + ) + }) + } + +
TypeRoom/PlayerOpened
{issue.categoryId}{issue.reportedUserName}{new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString()}
+ + ); +} diff --git a/src/views/mod-tools/views/tickets/open-issues/ModToolsOpenIssuesTabView.types.ts b/src/views/mod-tools/views/tickets/open-issues/ModToolsOpenIssuesTabView.types.ts new file mode 100644 index 00000000..45a80d7d --- /dev/null +++ b/src/views/mod-tools/views/tickets/open-issues/ModToolsOpenIssuesTabView.types.ts @@ -0,0 +1,6 @@ +import { IssueMessageData } from '@nitrots/nitro-renderer'; + +export interface ModToolsOpenIssuesTabViewProps +{ + openIssues: IssueMessageData[]; +} diff --git a/src/views/mod-tools/views/tickets/picked-issues/ModToolsPickedIssuesTabView.tsx b/src/views/mod-tools/views/tickets/picked-issues/ModToolsPickedIssuesTabView.tsx new file mode 100644 index 00000000..988a0fd9 --- /dev/null +++ b/src/views/mod-tools/views/tickets/picked-issues/ModToolsPickedIssuesTabView.tsx @@ -0,0 +1,35 @@ +import { FC } from 'react'; +import { ModToolsPickedIssuesTabViewProps } from './ModToolsPickedIssuesTabView.types'; + +export const ModToolsPickedIssuesTabView: FC = props => +{ + const { pickedIssues = null } = props; + + return ( + <> + + + + + + + + + + + {pickedIssues.map(issue => + { + return ( + + + + + + ) + }) + } + +
TypeRoom/PlayerOpenedPicker
{issue.categoryId}{issue.reportedUserName}{new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString()}{issue.pickerUserName}
+ + ); +} diff --git a/src/views/mod-tools/views/tickets/picked-issues/ModToolsPickedIssuesTabView.types.ts b/src/views/mod-tools/views/tickets/picked-issues/ModToolsPickedIssuesTabView.types.ts new file mode 100644 index 00000000..c69b1772 --- /dev/null +++ b/src/views/mod-tools/views/tickets/picked-issues/ModToolsPickedIssuesTabView.types.ts @@ -0,0 +1,6 @@ +import { IssueMessageData } from '@nitrots/nitro-renderer'; + +export interface ModToolsPickedIssuesTabViewProps +{ + pickedIssues: IssueMessageData[]; +} diff --git a/src/views/mod-tools/views/user/ModToolsUserView.tsx b/src/views/mod-tools/views/user/ModToolsUserView.tsx deleted file mode 100644 index d3a2f076..00000000 --- a/src/views/mod-tools/views/user/ModToolsUserView.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { FC } from 'react'; -import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout'; -import { ModToolsUserViewProps } from './ModToolsUserView.types'; - -export const ModToolsUserView: FC = props => -{ - return ( - - {} } /> - - - - - ); -} diff --git a/src/views/mod-tools/views/user/ModToolsUserView.types.ts b/src/views/mod-tools/views/user/ModToolsUserView.types.ts deleted file mode 100644 index 55b9477c..00000000 --- a/src/views/mod-tools/views/user/ModToolsUserView.types.ts +++ /dev/null @@ -1,2 +0,0 @@ -export interface ModToolsUserViewProps -{} diff --git a/src/views/mod-tools/views/user/user-chatlog/ModToolsUserChatlogView.tsx b/src/views/mod-tools/views/user/user-chatlog/ModToolsUserChatlogView.tsx new file mode 100644 index 00000000..705cc690 --- /dev/null +++ b/src/views/mod-tools/views/user/user-chatlog/ModToolsUserChatlogView.tsx @@ -0,0 +1,41 @@ +import { ChatRecordData, GetUserChatlogMessageComposer, UserChatlogEvent } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useState } from 'react'; +import { CreateMessageHook, SendMessageHook } from '../../../../../hooks'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout'; +import { ChatlogView } from '../../chatlog/ChatlogView'; +import { ModToolsUserChatlogViewProps } from './ModToolsUserChatlogView.types'; + +export const ModToolsUserChatlogView: FC = props => +{ + const { userId = null, onCloseClick = null } = props; + const [userChatlog, setUserChatlog] = useState(null); + const [username, setUsername] = useState(null); + + useEffect(() => + { + SendMessageHook(new GetUserChatlogMessageComposer(userId)); + }, [userId]); + + const onModtoolUserChatlogEvent = useCallback((event: UserChatlogEvent) => + { + const parser = event.getParser(); + + if(!parser || parser.data.userId !== userId) return; + + setUsername(parser.data.username); + setUserChatlog(parser.data.roomChatlogs); + }, [setUsername, setUserChatlog, userId]); + + CreateMessageHook(UserChatlogEvent, onModtoolUserChatlogEvent); + + return ( + + onCloseClick()} /> + + {userChatlog && + + } + + + ); +} diff --git a/src/views/mod-tools/views/user/user-chatlog/ModToolsUserChatlogView.types.ts b/src/views/mod-tools/views/user/user-chatlog/ModToolsUserChatlogView.types.ts new file mode 100644 index 00000000..c8eea569 --- /dev/null +++ b/src/views/mod-tools/views/user/user-chatlog/ModToolsUserChatlogView.types.ts @@ -0,0 +1,5 @@ +export interface ModToolsUserChatlogViewProps +{ + userId: number; + onCloseClick: () => void; +} diff --git a/src/views/mod-tools/views/user/user-info/ModToolsUserView.scss b/src/views/mod-tools/views/user/user-info/ModToolsUserView.scss new file mode 100644 index 00000000..ddc24350 --- /dev/null +++ b/src/views/mod-tools/views/user/user-info/ModToolsUserView.scss @@ -0,0 +1,23 @@ +.nitro-mod-tools-user { + width: 350px; + height: 370px; + + .username { + color: #1E7295; + text-decoration: underline; + } + + .table { + color: $black; + + > :not(caption) > * > * { + box-shadow: none; + border-bottom: 1px solid rgba(0, 0, 0, .2); + } + + &.table-striped > tbody > tr:nth-of-type(odd) { + color: $black; + background: rgba(0, 0, 0, .05); + } + } +} diff --git a/src/views/mod-tools/views/user/user-info/ModToolsUserView.tsx b/src/views/mod-tools/views/user/user-info/ModToolsUserView.tsx new file mode 100644 index 00000000..24f8c876 --- /dev/null +++ b/src/views/mod-tools/views/user/user-info/ModToolsUserView.tsx @@ -0,0 +1,153 @@ +import { FriendlyTime, GetModeratorUserInfoMessageComposer, ModeratorUserInfoData, ModeratorUserInfoEvent } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { LocalizeText } from '../../../../../api'; +import { ModToolsOpenUserChatlogEvent } from '../../../../../events/mod-tools/ModToolsOpenUserChatlogEvent'; +import { CreateMessageHook, dispatchUiEvent, SendMessageHook } from '../../../../../hooks'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardView, NitroLayoutButton, NitroLayoutGrid, NitroLayoutGridColumn } from '../../../../../layout'; +import { ModToolsUserModActionView } from '../user-mod-action/ModToolsUserModActionView'; +import { ModToolsUserRoomVisitsView } from '../user-room-visits/ModToolsUserRoomVisitsView'; +import { ModToolsSendUserMessageView } from '../user-sendmessage/ModToolsSendUserMessageView'; +import { ModToolsUserViewProps } from './ModToolsUserView.types'; + +export const ModToolsUserView: FC = props => +{ + const { onCloseClick = null, userId = null } = props; + const [ userInfo, setUserInfo ] = useState(null); + const [ sendMessageVisible, setSendMessageVisible ] = useState(false); + const [ modActionVisible, setModActionVisible ] = useState(false); + const [ roomVisitsVisible, setRoomVisitsVisible ] = useState(false); + + useEffect(() => + { + SendMessageHook(new GetModeratorUserInfoMessageComposer(userId)); + }, [ userId ]); + + const onModtoolUserInfoEvent = useCallback((event: ModeratorUserInfoEvent) => + { + const parser = event.getParser(); + + if(!parser || parser.data.userId !== userId) return; + + setUserInfo(parser.data); + }, [setUserInfo, userId]); + + CreateMessageHook(ModeratorUserInfoEvent, onModtoolUserInfoEvent); + + const userProperties = useMemo(() => + { + if(!userInfo) return null; + + return [ + { + localeKey: 'modtools.userinfo.userName', + value: userInfo.userName, + showOnline: true + }, + { + localeKey: 'modtools.userinfo.cfhCount', + value: userInfo.cfhCount.toString() + }, + { + localeKey: 'modtools.userinfo.abusiveCfhCount', + value: userInfo.abusiveCfhCount.toString() + }, + { + localeKey: 'modtools.userinfo.cautionCount', + value: userInfo.cautionCount.toString() + }, + { + localeKey: 'modtools.userinfo.banCount', + value: userInfo.banCount.toString() + }, + { + localeKey: 'modtools.userinfo.lastSanctionTime', + value: userInfo.lastSanctionTime + }, + { + localeKey: 'modtools.userinfo.tradingLockCount', + value: userInfo.tradingLockCount.toString() + }, + { + localeKey: 'modtools.userinfo.tradingExpiryDate', + value: userInfo.tradingExpiryDate + }, + { + localeKey: 'modtools.userinfo.minutesSinceLastLogin', + value: FriendlyTime.format(userInfo.minutesSinceLastLogin * 60, '.ago', 2) + }, + { + localeKey: 'modtools.userinfo.lastPurchaseDate', + value: userInfo.lastPurchaseDate + }, + { + localeKey: 'modtools.userinfo.primaryEmailAddress', + value: userInfo.primaryEmailAddress + }, + { + localeKey: 'modtools.userinfo.identityRelatedBanCount', + value: userInfo.identityRelatedBanCount.toString() + }, + { + localeKey: 'modtools.userinfo.registrationAgeInMinutes', + value: FriendlyTime.format(userInfo.registrationAgeInMinutes * 60, '.ago', 2) + }, + { + localeKey: 'modtools.userinfo.userClassification', + value: userInfo.userClassification + } + ]; + }, [ userInfo ]); + + if(!userInfo) return null; + + return ( + <> + + onCloseClick() } /> + + + + + + { userProperties.map( (property, index) => + { + + return ( + + + + + ); + }) } + +
{ LocalizeText(property.localeKey) } + { property.value } + { property.showOnline && } +
+
+ + dispatchUiEvent(new ModToolsOpenUserChatlogEvent(userId)) }> + Room Chat + + setSendMessageVisible(!sendMessageVisible) }> + Send Message + + setRoomVisitsVisible(!roomVisitsVisible) }> + Room Visits + + setModActionVisible(!modActionVisible) }> + Mod Action + + +
+
+
+ { sendMessageVisible && + setSendMessageVisible(false) } /> } + { modActionVisible && + setModActionVisible(false) } /> } + { roomVisitsVisible && + setRoomVisitsVisible(false) } /> } + + ); +} diff --git a/src/views/mod-tools/views/user/user-info/ModToolsUserView.types.ts b/src/views/mod-tools/views/user/user-info/ModToolsUserView.types.ts new file mode 100644 index 00000000..534d647d --- /dev/null +++ b/src/views/mod-tools/views/user/user-info/ModToolsUserView.types.ts @@ -0,0 +1,5 @@ +export interface ModToolsUserViewProps +{ + userId: number; + onCloseClick: () => void; +} diff --git a/src/views/mod-tools/views/user/user-mod-action/ModToolsUserModActionView.tsx b/src/views/mod-tools/views/user/user-mod-action/ModToolsUserModActionView.tsx new file mode 100644 index 00000000..09454bdc --- /dev/null +++ b/src/views/mod-tools/views/user/user-mod-action/ModToolsUserModActionView.tsx @@ -0,0 +1,201 @@ +import { CallForHelpTopicData, DefaultSanctionMessageComposer, ModAlertMessageComposer, ModBanMessageComposer, ModKickMessageComposer, ModMessageMessageComposer, ModMuteMessageComposer, ModTradingLockMessageComposer } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useMemo, useState } from 'react'; +import { LocalizeText } from '../../../../../api'; +import { NotificationAlertEvent } from '../../../../../events'; +import { dispatchUiEvent, SendMessageHook } from '../../../../../hooks'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../layout'; +import { useModToolsContext } from '../../../context/ModToolsContext'; +import { ModActionDefinition } from '../../../utils/ModActionDefinition'; +import { ModToolsUserModActionViewProps } from './ModToolsUserModActionView.types'; + +const actions = [ + new ModActionDefinition(1, 'Alert', ModActionDefinition.ALERT, 1, 0), + new ModActionDefinition(2, 'Mute 1h', ModActionDefinition.MUTE, 2, 0), + new ModActionDefinition(4, 'Ban 7 days', ModActionDefinition.BAN, 4, 0), + new ModActionDefinition(3, 'Ban 18h', ModActionDefinition.BAN, 3, 0), + new ModActionDefinition(5, 'Ban 30 days (step 1)', ModActionDefinition.BAN, 5, 0), + new ModActionDefinition(7, 'Ban 30 days (step 2)', ModActionDefinition.BAN, 7, 0), + new ModActionDefinition(6, 'Ban 100 years', ModActionDefinition.BAN, 6, 0), + new ModActionDefinition(106, 'Ban avatar-only 100 years', ModActionDefinition.BAN, 6, 0), + new ModActionDefinition(101, 'Kick', ModActionDefinition.KICK, 0, 0), + new ModActionDefinition(102, 'Lock trade 1 week', ModActionDefinition.TRADE_LOCK, 0, 168), + new ModActionDefinition(104, 'Lock trade permanent', ModActionDefinition.TRADE_LOCK, 0, 876000), + new ModActionDefinition(105, 'Message', ModActionDefinition.MESSAGE, 0, 0), +]; + +export const ModToolsUserModActionView: FC = props => +{ + const { user = null, onCloseClick = null } = props; + const { modToolsState = null, dispatchModToolsState = null } = useModToolsContext(); + const { cfhCategories = null, settings = null } = modToolsState; + const [ selectedTopic, setSelectedTopic ] = useState(-1); + const [ selectedAction, setSelectedAction ] = useState(-1); + const [ message, setMessage ] = useState(''); + + const topics = useMemo(() => + { + const values: CallForHelpTopicData[] = []; + + if(!cfhCategories) return values; + + for(let category of cfhCategories) + { + for(let topic of category.topics) + { + values.push(topic) + } + } + + return values; + }, [cfhCategories]); + + const sendDefaultSanction = useCallback(() => + { + SendMessageHook(new DefaultSanctionMessageComposer(user.userId, selectedTopic, message)); + onCloseClick(); + }, [message, onCloseClick, selectedTopic, user.userId]); + + const sendSanction = useCallback(() => + { + if( (selectedTopic === -1) || (selectedAction === -1) ) + { + dispatchUiEvent(new NotificationAlertEvent(['You must select a CFH topic and Sanction'], null, null, null, 'Error', null)); + return; + } + + if(!settings || !settings.cfhPermission) + { + dispatchUiEvent(new NotificationAlertEvent(['You do not have permission to do this'], null, null, null, 'Error', null)); + return; + } + + const category = topics[selectedTopic]; + const sanction = actions[selectedAction]; + + if(!category) + { + dispatchUiEvent(new NotificationAlertEvent(['You must select a CFH topic'], null, null, null, 'Error', null)); + return; + } + + if(!sanction) + { + dispatchUiEvent(new NotificationAlertEvent(['You must select a sanction'], null, null, null, 'Error', null)); + return; + } + + const messageOrDefault = message.trim().length === 0 ? LocalizeText('help.cfh.topic.' + category.id) : message; + + switch(sanction.actionType) + { + case ModActionDefinition.ALERT: + + if(!settings.alertPermission) + { + dispatchUiEvent(new NotificationAlertEvent(['You have insufficient permissions.'], null, null, null, 'Error', null)); + return; + } + + if(message.trim().length === 0) + { + dispatchUiEvent(new NotificationAlertEvent(['Please write a message to user.'], null, null, null, 'Error', null)); + return; + } + + SendMessageHook(new ModAlertMessageComposer(user.userId, message, category.id)); + + break; + case ModActionDefinition.MUTE: + SendMessageHook(new ModMuteMessageComposer(user.userId, messageOrDefault, category.id)); + + break; + case ModActionDefinition.BAN: + + if(!settings.banPermission) + { + dispatchUiEvent(new NotificationAlertEvent(['You have insufficient permissions.'], null, null, null, 'Error', null)); + return; + } + + SendMessageHook(new ModBanMessageComposer(user.userId, messageOrDefault, category.id, selectedAction, (sanction.actionId === 106))); + + break; + + case ModActionDefinition.KICK: + + if(!settings.kickPermission) + { + dispatchUiEvent(new NotificationAlertEvent(['You have insufficient permissions.'], null, null, null, 'Error', null)); + return; + } + + SendMessageHook(new ModKickMessageComposer(user.userId, messageOrDefault, category.id)); + + break; + + case ModActionDefinition.TRADE_LOCK: + { + const numSeconds = sanction.actionLengthHours * 60; + SendMessageHook(new ModTradingLockMessageComposer(user.userId, messageOrDefault, numSeconds, category.id)); + } + break; + + case ModActionDefinition.MESSAGE: + + if(message.trim().length === 0) + { + dispatchUiEvent(new NotificationAlertEvent(['Please write a message to user.'], null, null, null, 'Error', null)); + return; + } + + SendMessageHook(new ModMessageMessageComposer(user.userId, message, category.id)); + + break; + } + + onCloseClick(); + }, [message, onCloseClick, selectedAction, selectedTopic, settings, topics, user.userId]); + + return ( + + onCloseClick() } /> + + { user && + <> +
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+ } +
+
+ ); +} diff --git a/src/views/notification-center/NotificationCenterMessageHandler.tsx b/src/views/notification-center/NotificationCenterMessageHandler.tsx index 9afc098d..76c68e65 100644 --- a/src/views/notification-center/NotificationCenterMessageHandler.tsx +++ b/src/views/notification-center/NotificationCenterMessageHandler.tsx @@ -60,7 +60,7 @@ export const NotificationCenterMessageHandler: FC = props => dispatchUiEvent(new UserSettingsUIEvent(UserSettingsUIEvent.TOGGLE_USER_SETTINGS)); }, []); + const handleHelpCenterClick = useCallback(() => + { + dispatchUiEvent(new HelpEvent(HelpEvent.TOGGLE_HELP_CENTER)); + }, []); + const displayedCurrencies = useMemo(() => { return GetConfiguration('system.currency.types', []); @@ -140,7 +146,7 @@ export const PurseView: FC<{}> = props =>
-
+
diff --git a/src/views/room/widgets/chat-input/ChatInputView.scss b/src/views/room/widgets/chat-input/ChatInputView.scss index d2814d44..650a2e73 100644 --- a/src/views/room/widgets/chat-input/ChatInputView.scss +++ b/src/views/room/widgets/chat-input/ChatInputView.scss @@ -10,6 +10,14 @@ padding-right: 10px; width: 100%; + @include media-breakpoint-down(sm) { + display: flex; + position: absolute; + bottom: 70px; + left: calc(100% / 3); + width: 200px; + } + &:before { content: ""; position: absolute; diff --git a/src/views/room/widgets/furniture/trophy/FurnitureTrophyView.tsx b/src/views/room/widgets/furniture/trophy/FurnitureTrophyView.tsx index 5f21e504..403a60e2 100644 --- a/src/views/room/widgets/furniture/trophy/FurnitureTrophyView.tsx +++ b/src/views/room/widgets/furniture/trophy/FurnitureTrophyView.tsx @@ -1,6 +1,6 @@ import { NitroEvent, RoomEngineTriggerWidgetEvent, RoomObjectVariable } from '@nitrots/nitro-renderer'; import { FC, useCallback, useState } from 'react'; -import { GetRoomEngine, RoomWidgetRoomObjectUpdateEvent } from '../../../../../api'; +import { GetRoomEngine, RoomWidgetUpdateRoomObjectEvent } from '../../../../../api'; import { CreateEventDispatcherHook } from '../../../../../hooks/events/event-dispatcher.base'; import { useRoomEngineEvent } from '../../../../../hooks/events/nitro/room/room-engine-event'; import { NitroLayoutTrophyView } from '../../../../../layout'; @@ -9,6 +9,7 @@ import { FurnitureTrophyData } from './FurnitureTrophyData'; export const FurnitureTrophyView: FC<{}> = props => { + const { eventDispatcher = null, widgetHandler = null } = useRoomContext(); const [ trophyData, setTrophyData ] = useState(null); @@ -39,8 +40,8 @@ export const FurnitureTrophyView: FC<{}> = props => setTrophyData(new FurnitureTrophyData(widgetEvent.objectId, widgetEvent.category, color, ownerName, trophyDate, trophyText)); return; } - case RoomWidgetRoomObjectUpdateEvent.FURNI_REMOVED: { - const widgetEvent = (event as RoomWidgetRoomObjectUpdateEvent); + case RoomWidgetUpdateRoomObjectEvent.FURNI_REMOVED: { + const widgetEvent = (event as RoomWidgetUpdateRoomObjectEvent); setTrophyData(prevState => { @@ -54,7 +55,7 @@ export const FurnitureTrophyView: FC<{}> = props => }, []); useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_TROPHY, onNitroEvent); - CreateEventDispatcherHook(RoomWidgetRoomObjectUpdateEvent.FURNI_REMOVED, widgetHandler.eventDispatcher, onNitroEvent); + CreateEventDispatcherHook(RoomWidgetUpdateRoomObjectEvent.FURNI_REMOVED, widgetHandler.eventDispatcher, onNitroEvent); const processAction = useCallback((type: string, value: string = null) => { diff --git a/src/views/room/widgets/room-tools/RoomToolsWidgetView.tsx b/src/views/room/widgets/room-tools/RoomToolsWidgetView.tsx index 250e979d..d52ef382 100644 --- a/src/views/room/widgets/room-tools/RoomToolsWidgetView.tsx +++ b/src/views/room/widgets/room-tools/RoomToolsWidgetView.tsx @@ -3,6 +3,7 @@ import classNames from 'classnames'; import { FC, useCallback, useState } from 'react'; import { LocalizeText, RoomWidgetZoomToggleMessage } from '../../../../api'; import { NavigatorEvent } from '../../../../events'; +import { ChatHistoryEvent } from '../../../../events/chat-history/ChatHistoryEvent'; import { dispatchUiEvent } from '../../../../hooks/events'; import { SendMessageHook } from '../../../../hooks/messages'; import { useRoomContext } from '../../context/RoomContext'; @@ -27,6 +28,8 @@ export const RoomToolsWidgetView: FC<{}> = props => setIsZoomedIn(value => !value); return; case 'chat_history': + dispatchUiEvent(new ChatHistoryEvent(ChatHistoryEvent.TOGGLE_CHAT_HISTORY)); + //setIsExpanded(false); close this ?? return; case 'like_room': if(isLiked) return; diff --git a/src/views/toolbar/ToolbarView.scss b/src/views/toolbar/ToolbarView.scss index 57401e81..779b8160 100644 --- a/src/views/toolbar/ToolbarView.scss +++ b/src/views/toolbar/ToolbarView.scss @@ -14,6 +14,11 @@ #toolbar-chat-input-container { margin: 0 10px; + + @include media-breakpoint-down(sm) { + width: 0px; + height: 0px + } } .navigation-items { diff --git a/src/views/toolbar/ToolbarView.tsx b/src/views/toolbar/ToolbarView.tsx index 34b1ac71..1e9fbab2 100644 --- a/src/views/toolbar/ToolbarView.tsx +++ b/src/views/toolbar/ToolbarView.tsx @@ -29,6 +29,7 @@ export const ToolbarView: FC = props => const [ unseenInventoryCount, setUnseenInventoryCount ] = useState(0); const [ unseenAchievementCount, setUnseenAchievementCount ] = useState(0); + const isMod = GetSessionDataManager().isModerator; const unseenFriendListCount = 0; const onUserInfoEvent = useCallback((event: UserInfoEvent) => @@ -200,9 +201,10 @@ export const ToolbarView: FC = props =>
handleToolbarItemClick(ToolbarViewItems.CAMERA_ITEM) }>
) } -
handleToolbarItemClick(ToolbarViewItems.MOD_TOOLS_ITEM) }> + { isMod && ( +
handleToolbarItemClick(ToolbarViewItems.MOD_TOOLS_ITEM) }> -
+
) }