refactor chat providing scalability and more precise text rendering
fix reconnect qs removal
This commit is contained in:
parent
e4fcf10057
commit
692ec89d9b
5 changed files with 158 additions and 72 deletions
|
|
@ -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>
|
||||
|
|
|
|||
209
lib/chat.js
209
lib/chat.js
|
|
@ -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>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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']) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue