feat: add support for /ping command, fix chat fading!

This commit is contained in:
Vitaly Turovsky 2025-06-27 22:06:10 +03:00
commit 0e4435ef91
6 changed files with 55 additions and 46 deletions

View file

@ -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;
}

View file

@ -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 <li className={Object.entries(classes).filter(([, val]) => val).map(([name]) => name).join(' ')}>
return <li className={Object.entries(classes).filter(([, val]) => 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 ({
</div>
)}
{messages.map((m) => (
<MessageLine key={reactKeyForMessage(m)} message={m} currentPlayerName={playerNameValidated} />
<MessageLine key={reactKeyForMessage(m)} message={m} currentPlayerName={playerNameValidated} chatOpened={opened} />
))}
</div> || undefined}
</div>

View file

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

View file

@ -34,6 +34,9 @@ export default () => {
fontFamily: 'minecraft, monospace',
textAlign: 'center',
zIndex: 1000,
position: 'fixed',
left: 0,
right: 0
}}>
<div style={{
fontSize: '16px',

View file

@ -120,7 +120,7 @@ const detectStorageConflicts = (): StorageConflict[] => {
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) => {

View file

@ -229,6 +229,7 @@ const App = () => {
<div />
</RobustPortal>
<EnterFullscreenButton />
<StorageConflictModal />
<InGameUi />
<RobustPortal to={document.querySelector('#ui-root')}>
<AllWidgets />
@ -248,7 +249,6 @@ const App = () => {
<SelectOption />
<CreditsAboutModal />
<StorageConflictModal />
<NoModalFoundProvider />
</RobustPortal>
<RobustPortal to={document.body}>