refactor chat providing scalability and more precise text rendering

fix reconnect qs removal
This commit is contained in:
Vitaly 2023-08-15 19:51:57 +03:00
commit 692ec89d9b
5 changed files with 158 additions and 72 deletions

View file

@ -27,6 +27,7 @@
<pmui-advanced-optionsscreen style="display: none;"></pmui-advanced-optionsscreen>
<pmui-titlescreen id="title-screen" style="display: none;"></pmui-titlescreen>
<pmui-notification></pmui-notification>
<context-menu id="context-menu"></context-menu>
</div>
</body>
</html>

View file

@ -1,6 +1,9 @@
//@ts-check
const { LitElement, html, css } = require('lit')
const { isMobile } = require('./menus/components/common')
const { activeModalStack, hideCurrentModal } = require('./globalState')
import { repeat } from 'lit/directives/repeat.js'
import { classMap } from 'lit/directives/class-map.js'
const styles = {
black: 'color:#000000',
@ -36,6 +39,11 @@ function colorShadow (hex, dim = 0.25) {
return `#${f(r)}${f(g)}${f(b)}`
}
/**
* @typedef {{text;color?;italic?;underlined?;strikethrough?;bold?}} MessagePart
* @typedef {{parts: MessagePart[], id, fading?, faded}} Message
*/
class ChatBox extends LitElement {
static get styles () {
return css`
@ -44,7 +52,7 @@ class ChatBox extends LitElement {
z-index: 10;
}
.chat-display-wrapper {
.chat-messages-wrapper {
bottom: 40px;
padding: 4px;
padding-left: 0;
@ -96,7 +104,7 @@ class ChatBox extends LitElement {
}
.chat-message {
display: block;
display: flex;
padding-left: 4px;
background-color: rgba(0, 0, 0, 0.5);
}
@ -114,40 +122,51 @@ class ChatBox extends LitElement {
transition: none !important;
}
.chat-message-chat-opened {
.chat.opened .chat-message {
opacity: 1 !important;
transition: none !important;
}
.chat-message-part {
white-space: pre-wrap;
}
`
}
render () {
return html`
<div id="chat-wrapper" class="chat-wrapper chat-display-wrapper">
<div class="chat" id="chat">
<li class="chat-message chat-message-fade chat-message-faded">Welcome to prismarine-web-client! Chat appears here.</li>
</div>
</div>
<div id="chat-wrapper2" class="chat-wrapper chat-input-wrapper">
<div class="chat" id="chat-input">
<input type="text" class="chat" id="chatinput" spellcheck="false" autocomplete="off"></input>
</div>
</div>
`
static get properties () {
return {
messages: {
type: Array
}
}
}
constructor () {
super()
this.chatHistoryPos = 0
this.chatHistory = []
this.messagesLimit = 200
/** @type {Message[]} */
this.messages = [{
parts: [
{
text: 'Welcome to prismarine-web-client! Chat appears here.',
}
],
id: 0,
fading: true,
faded: true,
}]
}
enableChat (isCommand) {
const chat = this.shadowRoot.querySelector('#chat')
const chatInput = this.shadowRoot.querySelector('#chatinput')
enableChat (initialText = '') {
const chat = this.shadowRoot.getElementById('chat-messages')
/** @type {HTMLInputElement} */
// @ts-ignore
const chatInput = this.shadowRoot.getElementById('chatinput')
this.shadowRoot.querySelector('#chat-wrapper2').classList.toggle('input-mobile', isMobile())
this.shadowRoot.querySelector('#chat-wrapper').classList.toggle('display-mobile', isMobile())
this.shadowRoot.getElementById('chat-wrapper2').classList.toggle('input-mobile', isMobile())
this.shadowRoot.getElementById('chat-wrapper').classList.toggle('display-mobile', isMobile())
activeModalStack.push(this)
@ -158,13 +177,13 @@ class ChatBox extends LitElement {
// Show extended chat history
chat.style.maxHeight = 'var(--chatHeight)'
chat.scrollTop = chat.scrollHeight // Stay bottom of the list
if (isCommand) { // handle commands
chatInput.value = '/'
}
// handle / and other snippets
chatInput.value = initialText
// Focus element
chatInput.focus()
this.chatHistoryPos = this.chatHistory.length
document.querySelector('#hud').shadowRoot.querySelector('#chat').shadowRoot.querySelectorAll('.chat-message').forEach(e => e.classList.add('chat-message-chat-opened'))
// to show
this.requestUpdate()
}
get inChat () {
@ -175,8 +194,10 @@ class ChatBox extends LitElement {
* @param {import('minecraft-protocol').Client} client
*/
init (client) {
const chat = this.shadowRoot.querySelector('#chat')
const chatInput = this.shadowRoot.querySelector('#chatinput')
const chat = this.shadowRoot.getElementById('chat-messages')
/** @type {HTMLInputElement} */
// @ts-ignore
const chatInput = this.shadowRoot.getElementById('chatinput')
// Show chat
chat.style.display = 'block'
@ -184,7 +205,6 @@ class ChatBox extends LitElement {
// Chat events
document.addEventListener('keydown', e => {
if (activeModalStack.slice(-1)[0] !== this) return
e = e || window.event
if (e.code === 'ArrowUp') {
if (this.chatHistoryPos === 0) return
chatInput.value = this.chatHistory[--this.chatHistoryPos] !== undefined ? this.chatHistory[this.chatHistoryPos] : ''
@ -199,16 +219,15 @@ class ChatBox extends LitElement {
const keyBindScrn = document.getElementById('keybinds-screen')
document.addEventListener('keypress', e => {
e = e || window.event
if (!this.inChat && activeModalStack.length === 0) {
keyBindScrn.keymaps.forEach(km => {
if (e.code === km.key) {
switch (km.defaultKey) {
case 'KeyT':
setTimeout(() => this.enableChat(false), 0)
setTimeout(() => this.enableChat(), 0)
break
case 'Slash':
setTimeout(() => this.enableChat(true), 0)
setTimeout(() => this.enableChat('/'), 0)
break
}
}
@ -238,21 +257,18 @@ class ChatBox extends LitElement {
// Hide extended chat history
chat.style.maxHeight = 'var(--chatHeight)'
chat.scrollTop = chat.scrollHeight // Stay bottom of the list
document.querySelector('#hud').shadowRoot.querySelector('#chat').shadowRoot.querySelectorAll('.chat-message').forEach(e => e.classList.remove('chat-message-chat-opened'))
this.requestUpdate()
return 'custom' // custom hide
}
this.hide()
client.on('chat', (packet) => {
// Reading of chat message
// Handle new message
const fullmessage = JSON.parse(packet.message.toString())
/** @type {MessagePart[]} */
const msglist = []
const colorF = (color) => {
return color.trim().startsWith('#') ? `color:${color}` : styles[color] ?? undefined
}
const readMsg = (msglist, msg) => {
const readMsg = (msg) => {
const styles = {
color: msg.color,
bold: !!msg.bold,
@ -264,30 +280,29 @@ class ChatBox extends LitElement {
if (msg.text) {
msglist.push({
...msg,
text: msg.text,
...styles
})
}
if (msg.translate) {
} else if (msg.translate) {
const tText = window.mcData.language[msg.translate] ?? msg.translate
if (msg.with) {
const splited = tText.split(/%s|%\d+\$s/g)
const splitted = tText.split(/%s|%\d+\$s/g)
let i = 0
splited.forEach((spl, j, arr) => {
msglist.push({ text: spl, ...styles })
splitted.forEach((part, j) => {
msglist.push({ text: part, ...styles })
if (j + 1 < arr.length) {
if (j + 1 < splitted.length) {
if (msg.with[i]) {
if (typeof msg.with[i] === 'string') {
readMsg(msglist, {
readMsg({
...styles,
text: msg.with[i]
})
} else {
readMsg(msglist, {
readMsg({
...styles,
...msg.with[i]
})
@ -298,6 +313,7 @@ class ChatBox extends LitElement {
})
} else {
msglist.push({
...msg,
text: tText,
...styles
})
@ -306,40 +322,95 @@ class ChatBox extends LitElement {
if (msg.extra) {
msg.extra.forEach(ex => {
readMsg(msglist, { ...styles, ...ex })
readMsg({ ...styles, ...ex })
})
}
}
readMsg(msglist, fullmessage)
readMsg(fullmessage)
const lastId = this.messages.at(-1)?.id ?? 0
this.messages = [...this.messages.slice(-this.messagesLimit), {
parts: msglist,
id: lastId + 1,
fading: false,
faded: false
}]
const message = this.messages.at(-1)
const li = document.createElement('li')
msglist.forEach(msg => {
const span = document.createElement('span')
span.appendChild(document.createTextNode(msg.text))
span.setAttribute(
'style',
`${msg.color ? colorF(msg.color.toLowerCase()) + `; text-shadow: 1px 1px 0px ${colorShadow(colorF(msg.color.toLowerCase()).replace('color:', ''))}` : styles.white}; ${msg.bold ? styles.bold + ';' : ''
}${msg.italic ? styles.italic + ';' : ''}${msg.strikethrough ? styles.strikethrough + ';' : ''
}${msg.underlined ? styles.underlined + ';' : ''}`
)
li.appendChild(span)
})
chat.appendChild(li)
chat.scrollTop = chat.scrollHeight // Stay bottom of the list
// fading
li.classList.add('chat-message')
if (this.inChat) {
li.classList.add('chat-message-chat-opened')
}
setTimeout(() => {
li.classList.add('chat-message-fadeout')
li.classList.add('chat-message-fade')
message.fading = true
this.requestUpdate()
setTimeout(() => {
li.classList.add('chat-message-faded')
message.faded = true
this.requestUpdate()
}, 3000)
}, 5000)
})
// todo support hover content below, {action: 'show_text', contents: {text}}, and some other types
// todo remove
window.dummyMessage = () => {
client.emit('chat', {
message: "{\"color\":\"yellow\",\"translate\":\"multiplayer.player.joined\",\"with\":[{\"insertion\":\"pviewer672\",\"clickEvent\":{\"action\":\"suggest_command\",\"value\":\"/tell pviewer672 \"},\"hoverEvent\":{\"action\":\"show_entity\",\"contents\":{\"type\":\"minecraft:player\",\"id\":\"ecd0eeb1-625e-3fea-b16e-cb449dcfa434\",\"name\":{\"text\":\"pviewer672\"}}},\"text\":\"pviewer672\"}]}",
position: 1,
sender: "00000000-0000-0000-0000-000000000000",
})
}
// window.dummyMessage()
}
renderMessagePart (/** @type {MessagePart} */{ bold, color, italic, strikethrough, text, underlined }) {
const colorF = (color) => {
return color.trim().startsWith('#') ? `color:${color}` : styles[color] ?? undefined
}
/** @type {string[]} */
const applyStyles = [
color ? colorF(color.toLowerCase()) + `; text-shadow: 1px 1px 0px ${colorShadow(colorF(color.toLowerCase()).replace('color:', ''))}` : styles.white,
italic && styles.italic,
bold && styles.bold,
italic && styles.italic,
underlined && styles.underlined,
strikethrough && styles.strikethrough
].filter(Boolean)
return html`
<span
class="chat-message-part"
style="${applyStyles.join(';')}"
>${text}</span>`
}
renderMessage (/** @type {Message} */message) {
const classes = {
'chat-message-fadeout': message.fading,
'chat-message-fade': message.fading,
'chat-message-faded': message.faded,
'chat-message': true
}
return html`
<li class=${classMap(classes)}>
${message.parts.map(msg => this.renderMessagePart(msg))}
</li>
`
}
render () {
return html`
<div id="chat-wrapper" class="chat-wrapper chat-messages-wrapper">
<div class="chat ${this.inChat ? 'opened' : ''}" id="chat-messages">
${repeat(this.messages, (m) => m.id, (m) => this.renderMessage(m))}
</div>
</div>
<div id="chat-wrapper2" class="chat-wrapper chat-input-wrapper">
<div class="chat" id="chat-input">
<input type="text" class="chat" id="chatinput" spellcheck="false" autocomplete="off"></input>
</div>
</div>
`
}
}

View file

@ -1,11 +1,13 @@
//@ts-check
import { proxy } from 'valtio'
import { pointerLock } from './utils'
// todo: refactor structure with support of hideNext=false
/**
* @typedef {(HTMLElement & Record<string, any>)} Modal
* @typedef {{callback, label}} ContextMenuItem
*/
/** @type {Modal[]} */
@ -74,3 +76,15 @@ export const hideCurrentModal = (_data = undefined, preActions = undefined) => {
}
}
}
// ---
export const currentContextMenu = proxy({ items: /** @type {ContextMenuItem[] | null} */[], x: 0, y: 0, })
export const showContextmenu = (/** @type {ContextMenuItem[]} */items, { clientX, clientY }) => {
Object.assign(currentContextMenu, {
items,
x: clientX,
y: clientY,
})
}

View file

@ -305,7 +305,7 @@ class Hud extends LitElement {
<div class="mobile-top-btns" id="mobile-top">
<button class="chat-btn" @click=${(e) => {
e.stopPropagation()
this.shadowRoot.querySelector('#chat').enableChat(false)
this.shadowRoot.querySelector('#chat').enableChat()
}}></button>
<button class="pause-btn" @click=${(e) => {
e.stopPropagation()

View file

@ -74,7 +74,7 @@ class LoadingErrorScreen extends LitElement {
const qs = new URLSearchParams(window.location.search)
// remove reconnect from qs
qs.delete('reconnect')
window.history.replaceState(null, null, qs.toString())
window.history.replaceState({}, '', `${window.location.pathname}?${qs.toString()}`)
}
this.hasError = false
if (activeModalStacks['main-menu']) {