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/views/Styles.scss b/src/views/Styles.scss index 4f31d9e5..f66d2a1a 100644 --- a/src/views/Styles.scss +++ b/src/views/Styles.scss @@ -20,3 +20,4 @@ @import './achievements/AchievementsView'; @import './user-settings/UserSettingsView'; @import './user-profile/UserProfileVew'; +@import './chat-history/ChatHistoryView'; diff --git a/src/views/chat-history/ChatHistoryMessageHandler.tsx b/src/views/chat-history/ChatHistoryMessageHandler.tsx new file mode 100644 index 00000000..9a0da789 --- /dev/null +++ b/src/views/chat-history/ChatHistoryMessageHandler.tsx @@ -0,0 +1,98 @@ +import { RoomInfoEvent, RoomSessionChatEvent, RoomSessionEvent } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useState } from 'react'; +import { GetRoomSession } from '../../api'; +import { ChatHistoryEvent } from '../../events/chat-history/ChatHistoryEvent'; +import { CreateMessageHook, dispatchUiEvent, useRoomSessionManagerEvent } from '../../hooks'; +import { useChatHistoryContext } from './context/ChatHistoryContext'; +import { ChatEntryType, ChatHistoryAction, CHAT_HISTORY_MAX, IChatEntry } from './reducers/ChatHistoryReducer'; +import { currentDate } from './utils/Utilities'; + +export const ChatHistoryMessageHandler: FC<{}> = props => +{ + const { chatHistoryState = null, dispatchChatHistoryState = null } = useChatHistoryContext(); + const { chats = null, roomHistory = null } = chatHistoryState; + + const [needsRoomInsert, setNeedsRoomInsert ] = useState(false); + + const addChatEntry = useCallback((entry: IChatEntry) => + { + const newChats = [...chats]; + + newChats.push(entry); + + //check for overflow + if(newChats.length > CHAT_HISTORY_MAX) + { + newChats.shift(); + } + + dispatchChatHistoryState({ + type: ChatHistoryAction.SET_CHATS, + payload: { + chats: newChats + } + }); + + dispatchUiEvent(new ChatHistoryEvent(ChatHistoryEvent.CHAT_HISTORY_CHANGED)); + + }, [chats, dispatchChatHistoryState]); + + 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: userData.webID, name: userData.name, look: userData.figure, message: event.message, timestamp: timeString, type: ChatEntryType.TYPE_CHAT }; + + 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: parser.data.roomId, name: parser.data.roomName, timestamp: currentDate(), type: ChatEntryType.TYPE_ROOM_INFO }; + + addChatEntry(chatEntry); + + setNeedsRoomInsert(false); + } + }, [addChatEntry, 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..ba3a4451 --- /dev/null +++ b/src/views/chat-history/ChatHistoryView.scss @@ -0,0 +1,32 @@ +.nitro-chat-history +{ + width: 250px; + + padding: 2px; + background-color: #1C323F; + border: 2px solid rgba(255, 255, 255, 0.5); + border-radius: 0.25rem; + + .container-fluid + { + .nitro-card-header + { + background-color: #3d5f6e; + color: #fff; + } + + background-color: #1C323F; + + } + + .chat-history-content + { + background-color: #1C323F; + + .chat-history-container + { + background-color: #1C323F; + min-height: 200px; + } + } +} diff --git a/src/views/chat-history/ChatHistoryView.tsx b/src/views/chat-history/ChatHistoryView.tsx new file mode 100644 index 00000000..a6f3ff3a --- /dev/null +++ b/src/views/chat-history/ChatHistoryView.tsx @@ -0,0 +1,128 @@ +import { FC, useCallback, useEffect, useReducer, useRef, useState } from 'react'; +import { AutoSizer, CellMeasurer, CellMeasurerCache, List, ListRowProps, ListRowRenderer } from 'react-virtualized'; +import { ChatHistoryEvent } from '../../events/chat-history/ChatHistoryEvent'; +import { useUiEvent } from '../../hooks'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../layout'; +import { ChatHistoryMessageHandler } from './ChatHistoryMessageHandler'; +import { ChatHistoryContextProvider } from './context/ChatHistoryContext'; +import { ChatEntryType, ChatHistoryReducer, initialChatHistory } from './reducers/ChatHistoryReducer'; + +export const ChatHistoryView: FC<{}> = props => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ chatHistoryState, dispatchChatHistoryState ] = useReducer(ChatHistoryReducer, initialChatHistory); + const { chats = null } = chatHistoryState; + const elementRef = useRef(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 = new CellMeasurerCache({ + defaultHeight: 25, + fixedWidth: true + }); + + const RowRenderer: ListRowRenderer = (props: ListRowProps) => + { + const item = chats[props.index]; + + return ( + +
+ {(item.type === ChatEntryType.TYPE_CHAT) && + <> +
{item.timestamp}
+
+ +
+
+ {item.message} +
+ + } + {(item.type === ChatEntryType.TYPE_ROOM_INFO) && + <> +
{item.timestamp}
+
+ {item.name} +
+
+ {item.message} +
+ + } + +
+
+ ); + }; + + useEffect(() => + { + if(elementRef && elementRef.current && isVisible) + { + elementRef.current.scrollToRow(chats.length - 1); + } + console.log(chats.length); + }, [chats, isVisible]) + + return ( + + + {isVisible && + + setIsVisible(false) } /> + +
+ + {({ height, width }) => + { + cache.clearAll(); + + return ( + + ) + } + } + +
+
+
+ } +
+ ); +} diff --git a/src/views/chat-history/context/ChatHistoryContext.tsx b/src/views/chat-history/context/ChatHistoryContext.tsx new file mode 100644 index 00000000..10c8ee3f --- /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, + dispatchChatHistoryState: 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..57cbbd91 --- /dev/null +++ b/src/views/chat-history/context/ChatHistoryContext.types.ts @@ -0,0 +1,13 @@ +import { Dispatch, ProviderProps } from 'react'; +import { IChatHistoryAction, IChatHistoryState } from '../reducers/ChatHistoryReducer'; + +export interface IChatHistoryContext +{ + chatHistoryState: IChatHistoryState; + dispatchChatHistoryState: Dispatch; +} + +export interface ChatHistoryContextProps extends ProviderProps +{ + +} diff --git a/src/views/chat-history/reducers/ChatHistoryReducer.tsx b/src/views/chat-history/reducers/ChatHistoryReducer.tsx new file mode 100644 index 00000000..4a51ff8c --- /dev/null +++ b/src/views/chat-history/reducers/ChatHistoryReducer.tsx @@ -0,0 +1,66 @@ +import { Reducer } from 'react'; + +export interface IChatHistoryState { + chats: IChatEntry[]; + roomHistory: IRoomHistoryEntry[]; +} + +export interface IChatEntry { + id: number; + name: string; + look?: string; + message?: string; + 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 interface IChatHistoryAction +{ + type: string; + payload: { + chats?: IChatEntry[]; + roomHistory?: IRoomHistoryEntry[]; + } +} + +export class ChatHistoryAction +{ + public static SET_CHATS: string = 'CHA_SET_CHATS'; + public static SET_ROOM_HISTORY: string = 'CHA_SET_ROOM_HISTORY'; +} + +export const initialChatHistory: IChatHistoryState = { + chats: [], + roomHistory: [] +}; + +export const CHAT_HISTORY_MAX = 1000; +export const ROOM_HISTORY_MAX = 10; + +export const ChatHistoryReducer: Reducer = (state, action) => +{ + switch(action.type) + { + case ChatHistoryAction.SET_CHATS: { + const chats = (action.payload.chats || state.chats || null); + + return { ...state, chats }; + } + case ChatHistoryAction.SET_ROOM_HISTORY: { + const roomHistory = (action.payload.roomHistory || state.roomHistory || null); + + return { ...state, roomHistory }; + } + } +} diff --git a/src/views/chat-history/utils/Utilities.ts b/src/views/chat-history/utils/Utilities.ts new file mode 100644 index 00000000..4661146a --- /dev/null +++ b/src/views/chat-history/utils/Utilities.ts @@ -0,0 +1,5 @@ +export const currentDate = () => +{ + const currentTime = new Date(); + return `${currentTime.getHours()}:${currentTime.getMinutes()}`; +} diff --git a/src/views/main/MainView.tsx b/src/views/main/MainView.tsx index 49e28a61..2a7e1427 100644 --- a/src/views/main/MainView.tsx +++ b/src/views/main/MainView.tsx @@ -7,6 +7,7 @@ import { AchievementsView } from '../achievements/AchievementsView'; import { AvatarEditorView } from '../avatar-editor/AvatarEditorView'; import { CameraWidgetView } from '../camera/CameraWidgetView'; import { CatalogView } from '../catalog/CatalogView'; +import { ChatHistoryView } from '../chat-history/ChatHistoryView'; import { FriendsView } from '../friends/FriendsView'; import { GroupsView } from '../groups/GroupsView'; import { HelpView } from '../help/HelpView'; @@ -58,6 +59,7 @@ export const MainView: FC = props => + 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;