diff --git a/src/assets/images/chat/chathistory_background.png b/src/assets/images/chat/chathistory_background.png new file mode 100644 index 0000000..301d951 Binary files /dev/null and b/src/assets/images/chat/chathistory_background.png differ diff --git a/src/components/chat-history/ChatHistoryView.tsx b/src/components/chat-history/ChatHistoryView.tsx index cd09434..e236df6 100644 --- a/src/components/chat-history/ChatHistoryView.tsx +++ b/src/components/chat-history/ChatHistoryView.tsx @@ -1,42 +1,57 @@ import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; import { FC, useEffect, useMemo, useRef, useState } from 'react'; import { ChatEntryType, LocalizeText } from '../../api'; -import { Flex, InfiniteScroll, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; +import { Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; import { useChatHistory } from '../../hooks'; import { NitroInput } from '../../layout'; -export const ChatHistoryView: FC<{}> = props => -{ - const [ isVisible, setIsVisible ] = useState(false); - const [ searchText, setSearchText ] = useState(''); +export const ChatHistoryView: FC<{}> = props => { + const [isVisible, setIsVisible] = useState(false); + const [searchText, setSearchText] = useState(''); const { chatHistory = [] } = useChatHistory(); const elementRef = useRef(null); + const isFirstRender = useRef(true); + const prevChatLength = useRef(0); - const filteredChatHistory = useMemo(() => - { - if(searchText.length === 0) return chatHistory; + const filteredChatHistory = useMemo(() => { + let result = chatHistory; + + if (searchText.length > 0) { + const text = searchText.toLowerCase(); + result = chatHistory.filter(entry => + (entry.message && entry.message.toLowerCase().includes(text)) || + (entry.name && entry.name.toLowerCase().includes(text)) + ); + } + + return [...result]; + }, [chatHistory, searchText]); - let text = searchText.toLowerCase(); + useEffect(() => { + if (!elementRef.current || !isVisible) return; - return chatHistory.filter(entry => ((entry.message && entry.message.toLowerCase().includes(text))) || (entry.name && entry.name.toLowerCase().includes(text))); - }, [ chatHistory, searchText ]); + const element = elementRef.current; + const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight); + const isAtBottom = maxScrollTop === 0 || Math.abs(element.scrollTop - maxScrollTop) <= 50; - useEffect(() => - { - if(elementRef && elementRef.current && isVisible) elementRef.current.scrollTop = elementRef.current.scrollHeight; - }, [ isVisible ]); + if (isFirstRender.current) { + element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' }); + isFirstRender.current = false; + } else if (filteredChatHistory.length > prevChatLength.current) { + element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' }); + } - useEffect(() => - { + prevChatLength.current = filteredChatHistory.length; + }, [filteredChatHistory, isVisible]); + + useEffect(() => { const linkTracker: ILinkEventTracker = { - linkReceived: (url: string) => - { + linkReceived: (url: string) => { const parts = url.split('/'); - if(parts.length < 2) return; + if (parts.length < 2) return; - switch(parts[1]) - { + switch (parts[1]) { case 'show': setIsVisible(true); return; @@ -44,7 +59,7 @@ export const ChatHistoryView: FC<{}> = props => setIsVisible(false); return; case 'toggle': - setIsVisible(prevValue => !prevValue); + setIsVisible(prevValue => !prevChatLength.current); return; } }, @@ -56,42 +71,44 @@ export const ChatHistoryView: FC<{}> = props => return () => RemoveLinkEventTracker(linkTracker); }, []); - if(!isVisible) return null; + if (!isVisible) return null; return ( - - setIsVisible(false) } /> - - setSearchText(event.target.value) } /> - - { - return ( - - { row.timestamp } - { (row.type === ChatEntryType.TYPE_CHAT) && -
- { (row.style === 0) && -
} -
+ + setIsVisible(false)} /> + + setSearchText(event.target.value)} /> +
+ {filteredChatHistory.map((row, index) => ( + + {row.timestamp} + {row.type === ChatEntryType.TYPE_CHAT && ( +
+
- { row.imageUrl && (row.imageUrl.length > 0) && -
} + {row.imageUrl && row.imageUrl.length > 0 && ( +
+ )}
- - + +
-
} - { (row.type === ChatEntryType.TYPE_ROOM_INFO) && +
+ )} + {row.type === ChatEntryType.TYPE_ROOM_INFO && ( <> - { row.name } - } + {row.name} + + )} - ); - } } rows={ filteredChatHistory } scrollToBottom={ true } /> + ))} +
); -}; +}; \ No newline at end of file diff --git a/src/css/chat/ChatHistoryView.css b/src/css/chat/ChatHistoryView.css new file mode 100644 index 0000000..d614ada --- /dev/null +++ b/src/css/chat/ChatHistoryView.css @@ -0,0 +1,31 @@ +/* ChatHistoryView.css */ +.nitro-chat-history { + width: 400px; + height: 200px; + background-color: #f0f0f0; + } + +/* Increase specificity by repeating the .nitro-chat-history class */ +.nitro-chat-history .nitro-card-content { + height: 100%; + background-image: url('@/assets/images/chat/chathistory_background.png'); + background-repeat: repeat; + background-size: auto; + background-color: #f0f0f0; +} + +/* Increase specificity for the slide-in animation */ +.nitro-chat-history .p-1.slide-in { + animation: slideIn 0.3s ease-out; +} + +@keyframes slideIn { + 0% { + transform: translateY(-20px); + opacity: 0; + } + 100% { + transform: translateY(0); + opacity: 1; + } +} \ No newline at end of file diff --git a/src/css/chat/chats.css b/src/css/chat/chats.css index c381e39..02ca2f8 100644 --- a/src/css/chat/chats.css +++ b/src/css/chat/chats.css @@ -853,4 +853,4 @@ background: url('@/assets/images/chat/chatbubbles/bubble_38_extra.png'); } } -} +} \ No newline at end of file diff --git a/src/hooks/chat-history/useChatHistory.ts b/src/hooks/chat-history/useChatHistory.ts index 348f5d4..83efe9a 100644 --- a/src/hooks/chat-history/useChatHistory.ts +++ b/src/hooks/chat-history/useChatHistory.ts @@ -3,6 +3,7 @@ import { useState } from 'react'; import { useBetween } from 'use-between'; import { ChatEntryType, ChatHistoryCurrentDate, IChatEntry, IRoomHistoryEntry, MessengerHistoryCurrentDate } from '../../api'; import { useMessageEvent, useNitroEvent } from '../events'; +import { useLocalStorage } from '../useLocalStorage'; const CHAT_HISTORY_MAX = 1000; const ROOM_HISTORY_MAX = 10; @@ -13,10 +14,10 @@ let MESSENGER_HISTORY_COUNTER: number = 0; const useChatHistoryState = () => { - const [ chatHistory, setChatHistory ] = useState([]); - const [ roomHistory, setRoomHistory ] = useState([]); - const [ messengerHistory, setMessengerHistory ] = useState([]); - const [ needsRoomInsert, setNeedsRoomInsert ] = useState(false); + const [ chatHistory, setChatHistory ] = useLocalStorage('chatHistory', []); + const [ roomHistory, setRoomHistory ] = useLocalStorage('roomHistory', []); + const [ messengerHistory, setMessengerHistory ] = useLocalStorage('messengerHistory', []); + const [ needsRoomInsert, setNeedsRoomInsert ] = useLocalStorage('needsRoomInsert', false); const addChatEntry = (entry: IChatEntry) => { diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts index 6282300..62576e8 100644 --- a/src/hooks/useLocalStorage.ts +++ b/src/hooks/useLocalStorage.ts @@ -2,20 +2,21 @@ import { NitroLogger } from '@nitrots/nitro-renderer'; import { Dispatch, SetStateAction, useState } from 'react'; import { GetLocalStorage, SetLocalStorage } from '../api'; +const userId = new URLSearchParams(window.location.search).get('userid') || 0; + const useLocalStorageState = (key: string, initialValue: T): [ T, Dispatch>] => { + key = userId ? `${ key }.${ userId }` : key; + const [ storedValue, setStoredValue ] = useState(() => { - if(typeof window === 'undefined') return initialValue; - try { - const item = GetLocalStorage(key); - + const item = typeof window !== 'undefined' ? GetLocalStorage(key) as T : undefined; return item ?? initialValue; } - catch (error) + catch(error) { return initialValue; } @@ -32,13 +33,13 @@ const useLocalStorageState = (key: string, initialValue: T): [ T, Dispatch