diff --git a/src/react/Chat.css b/src/react/Chat.css index f1e92338..47394948 100644 --- a/src/react/Chat.css +++ b/src/react/Chat.css @@ -189,23 +189,22 @@ input[type=text], background-color: rgba(0, 0, 0, 0.5); list-style: none; overflow-wrap: break-word; -} - -.chat-message-fadeout { opacity: 1; - transition: all 3s; } -.chat-message-fade { +.chat-message-fading { opacity: 0; + transition: opacity 3s ease-in-out; } .chat-message-faded { - transition: none !important; + display: none; } +/* Ensure messages are always visible when chat is open */ .chat.opened .chat-message { opacity: 1 !important; + display: block !important; transition: none !important; } diff --git a/src/react/Chat.tsx b/src/react/Chat.tsx index 41a6cb6f..7c1c8633 100644 --- a/src/react/Chat.tsx +++ b/src/react/Chat.tsx @@ -11,19 +11,37 @@ import { useScrollBehavior } from './hooks/useScrollBehavior' export type Message = { parts: MessageFormatPart[], id: number - fading?: boolean - faded?: boolean + timestamp?: number } -const MessageLine = ({ message, currentPlayerName }: { message: Message, currentPlayerName?: string }) => { +const MessageLine = ({ message, currentPlayerName, chatOpened }: { message: Message, currentPlayerName?: string, chatOpened?: boolean }) => { + const [fadeState, setFadeState] = useState<'visible' | 'fading' | 'faded'>('visible') + + useEffect(() => { + // Start fading after 5 seconds + const fadeTimeout = setTimeout(() => { + setFadeState('fading') + }, 5000) + + // Remove after fade animation (3s) completes + const removeTimeout = setTimeout(() => { + setFadeState('faded') + }, 8000) + + // Cleanup timeouts if component unmounts + return () => { + clearTimeout(fadeTimeout) + clearTimeout(removeTimeout) + } + }, []) // Empty deps array since we only want this to run once when message is added + const classes = { - 'chat-message-fadeout': message.fading, - 'chat-message-fade': message.fading, - 'chat-message-faded': message.faded, - 'chat-message': true + 'chat-message': true, + 'chat-message-fading': !chatOpened && fadeState === 'fading', + 'chat-message-faded': !chatOpened && fadeState === 'faded' } - return
  • val).map(([name]) => name).join(' ')}> + return
  • val).map(([name]) => name).join(' ')} data-time={message.timestamp ? new Date(message.timestamp).toLocaleString('en-US', { hour12: false }) : undefined}> {message.parts.map((msg, i) => { // Check if this is a text part that might contain a mention if (msg.text && currentPlayerName) { @@ -70,17 +88,6 @@ export const chatInputValueGlobal = proxy({ value: '' }) -export const fadeMessage = (message: Message, initialTimeout: boolean, requestUpdate: () => void) => { - setTimeout(() => { - message.fading = true - requestUpdate() - setTimeout(() => { - message.faded = true - requestUpdate() - }, 3000) - }, initialTimeout ? 5000 : 0) -} - export default ({ messages, opacity = 1, @@ -372,7 +379,7 @@ export default ({ )} {messages.map((m) => ( - + ))} || undefined} diff --git a/src/react/ChatProvider.tsx b/src/react/ChatProvider.tsx index 83691b83..0bb13285 100644 --- a/src/react/ChatProvider.tsx +++ b/src/react/ChatProvider.tsx @@ -5,7 +5,7 @@ import { getBuiltinCommandsList, tryHandleBuiltinCommand } from '../builtinComma import { gameAdditionalState, hideCurrentModal, miscUiState } from '../globalState' import { options } from '../optionsStorage' import { viewerVersionState } from '../viewerConnector' -import Chat, { Message, fadeMessage } from './Chat' +import Chat, { Message } from './Chat' import { useIsModalActive } from './utilsApp' import { hideNotification, notificationProxy, showNotification } from './NotificationProvider' import { getServerIndex, updateLoadedServerData } from './serversStorage' @@ -16,6 +16,7 @@ export default () => { const [messages, setMessages] = useState([] as Message[]) const isChatActive = useIsModalActive('chat') const lastMessageId = useRef(0) + const lastPingTime = useRef(0) const usingTouch = useSnapshot(miscUiState).currentTouch const { chatSelect, messagesLimit, chatOpacity, chatOpacityOpened, chatVanillaRestrictions, debugChatScroll, chatPingExtension } = useSnapshot(options) const isUsingMicrosoftAuth = useMemo(() => !!lastConnectOptions.value?.authenticatedAccount, []) @@ -29,18 +30,23 @@ export default () => { jsonMsg = jsonMsg['unsigned'] } const parts = formatMessage(jsonMsg) + const messageText = parts.map(part => part.text).join('') + + // Handle ping response + if (messageText === 'Pong!' && lastPingTime.current > 0) { + const latency = Date.now() - lastPingTime.current + parts.push({ text: ` Latency: ${latency}ms`, color: '#00ff00' }) + lastPingTime.current = 0 + } setMessages(m => { lastMessageId.current++ const newMessage: Message = { parts, id: lastMessageId.current, - faded: false, + timestamp: Date.now() } - fadeMessage(newMessage, true, () => { - // eslint-disable-next-line max-nested-callbacks - setMessages(m => [...m]) - }) + return [...m, newMessage].slice(-messagesLimit) }) }) @@ -61,6 +67,11 @@ export default () => { return players.filter(name => (!value || name.toLowerCase().includes(value.toLowerCase())) && name !== bot.username).map(name => `@${name}`) }} sendMessage={async (message) => { + // Record ping command time + if (message === '/ping') { + lastPingTime.current = Date.now() + } + const builtinHandled = tryHandleBuiltinCommand(message) if (getServerIndex() !== undefined && (message.startsWith('/login') || message.startsWith('/register'))) { showNotification('Click here to save your password in browser for auto-login', undefined, false, undefined, () => { diff --git a/src/react/StorageConflictModal.tsx b/src/react/StorageConflictModal.tsx index 0b4224f3..ac78d90a 100644 --- a/src/react/StorageConflictModal.tsx +++ b/src/react/StorageConflictModal.tsx @@ -34,6 +34,9 @@ export default () => { fontFamily: 'minecraft, monospace', textAlign: 'center', zIndex: 1000, + position: 'fixed', + left: 0, + right: 0 }}>
    { const localParsed = JSON.parse(localStorageValue) const cookieParsed = JSON.parse(cookieValue) - if (localParsed?.migrated) { + if (localStorage.getItem(`${localStorageKey}:migrated`)) { continue } @@ -309,18 +309,7 @@ const markLocalStorageAsMigrated = (key: keyof StorageData) => { return } - const data = localStorage.getItem(localStorageKey) - if (data) { - try { - const parsed = JSON.parse(data) - localStorage.setItem( - localStorageKey, JSON.stringify(typeof parsed === 'object' ? { - ...parsed, migrated: Date.now() - } : { data: parsed, migrated: Date.now() }) - ) - } catch (err) { - } - } + localStorage.setItem(`${localStorageKey}:migrated`, 'true') } const saveKey = (key: keyof StorageData) => { diff --git a/src/reactUi.tsx b/src/reactUi.tsx index 59ed9124..b15cb79d 100644 --- a/src/reactUi.tsx +++ b/src/reactUi.tsx @@ -229,6 +229,7 @@ const App = () => {
    + @@ -248,7 +249,6 @@ const App = () => { -