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 = () => {
-