148 lines
5.1 KiB
TypeScript
148 lines
5.1 KiB
TypeScript
import { ComponentProps } from 'react'
|
|
import { render } from '@xmcl/text-component'
|
|
import { noCase } from 'change-case'
|
|
import mojangson from 'mojangson'
|
|
import { openURL } from 'renderer/viewer/lib/simpleUtils'
|
|
import { MessageFormatPart } from '../chatUtils'
|
|
import { chatInputValueGlobal } from './Chat'
|
|
import './MessageFormatted.css'
|
|
import { showOptionsModal } from './SelectOption'
|
|
|
|
const hoverItemToText = (hoverEvent: MessageFormatPart['hoverEvent']) => {
|
|
try {
|
|
if (!hoverEvent) return undefined
|
|
const contents = hoverEvent['contents'] ?? hoverEvent.value
|
|
if (typeof contents.text === 'string' && contents.text.startsWith('{')) {
|
|
Object.assign(contents, mojangson.simplify(mojangson.parse(contents.text)))
|
|
}
|
|
if (typeof contents === 'string') return contents
|
|
// if (hoverEvent.action === 'show_text') {
|
|
// return contents
|
|
// }
|
|
if (hoverEvent.action === 'show_item') {
|
|
return contents.id
|
|
}
|
|
if (hoverEvent.action === 'show_entity') {
|
|
let str = noCase(contents.type.replace('minecraft:', ''))
|
|
if (contents.name) str += `: ${contents.name.text}`
|
|
return str
|
|
}
|
|
} catch (err) {
|
|
// todo report critical error
|
|
reportError?.('Failed to parse message hover' + err.message)
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
const clickEventToProps = (clickEvent: MessageFormatPart['clickEvent']) => {
|
|
if (!clickEvent) return
|
|
if (clickEvent.action === 'run_command' || clickEvent.action === 'suggest_command') {
|
|
return {
|
|
onClick () {
|
|
chatInputValueGlobal.value = clickEvent.value
|
|
}
|
|
}
|
|
}
|
|
if (clickEvent.action === 'open_url' || clickEvent.action === 'open_file') {
|
|
return {
|
|
async onClick () {
|
|
const promptMessageText = `Open "${clickEvent.value}"?`
|
|
const confirm = await showOptionsModal(promptMessageText, ['Open', 'Copy'], {
|
|
cancel: true
|
|
})
|
|
if (confirm === 'Open') {
|
|
openURL(clickEvent.value)
|
|
} else if (confirm === 'Copy') {
|
|
void navigator.clipboard.writeText(clickEvent.value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (clickEvent.action === 'copy_to_clipboard') {
|
|
return {
|
|
onClick () {
|
|
void navigator.clipboard.writeText(clickEvent.value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const MessagePart = ({ part, ...props }: { part: MessageFormatPart } & ComponentProps<'span'>) => {
|
|
|
|
const { color, italic, bold, underlined, strikethrough, text, clickEvent, hoverEvent, obfuscated } = part
|
|
|
|
const clickProps = clickEventToProps(clickEvent)
|
|
const hoverMessageRaw = hoverItemToText(hoverEvent)
|
|
const hoverItemText = hoverMessageRaw && typeof hoverMessageRaw !== 'string' ? render(hoverMessageRaw).children.map(child => child.component.text).join('') : hoverMessageRaw
|
|
|
|
const applyStyles = [
|
|
clickProps && messageFormatStylesMap.clickEvent,
|
|
color ? colorF(color.toLowerCase()) + `; text-shadow: 1px 1px 0px ${getColorShadow(colorF(color.toLowerCase()).replace('color:', ''))}` : messageFormatStylesMap.white,
|
|
italic && messageFormatStylesMap.italic,
|
|
bold && messageFormatStylesMap.bold,
|
|
italic && messageFormatStylesMap.italic,
|
|
underlined && messageFormatStylesMap.underlined,
|
|
strikethrough && messageFormatStylesMap.strikethrough,
|
|
obfuscated && messageFormatStylesMap.obfuscated
|
|
].filter(a => a !== false && a !== undefined).filter(Boolean)
|
|
|
|
return <span title={hoverItemText} style={parseInlineStyle(applyStyles.join(';'))} {...clickProps} {...props}>{text}</span>
|
|
}
|
|
|
|
export default ({ parts, className }: { parts: readonly MessageFormatPart[], className?: string }) => {
|
|
return (
|
|
<span className={`formatted-message ${className ?? ''}`}>
|
|
{parts.map((part, i) => <MessagePart key={i} part={part} />)}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
const colorF = (color) => {
|
|
return color.trim().startsWith('#') ? `color:${color}` : messageFormatStylesMap[color] ?? undefined
|
|
}
|
|
|
|
export function getColorShadow (hex, dim = 0.25) {
|
|
const color = parseInt(hex.replace('#', ''), 16)
|
|
|
|
const r = Math.trunc((color >> 16 & 0xFF) * dim)
|
|
const g = Math.trunc((color >> 8 & 0xFF) * dim)
|
|
const b = Math.trunc((color & 0xFF) * dim)
|
|
|
|
const f = (c) => ('00' + c.toString(16)).slice(-2)
|
|
return `#${f(r)}${f(g)}${f(b)}`
|
|
}
|
|
|
|
export function parseInlineStyle (style: string): Record<string, any> {
|
|
const obj: Record<string, any> = {}
|
|
for (const rule of style.split(';')) {
|
|
const [prop, value] = rule.split(':')
|
|
const cssInJsProp = prop.trim().replaceAll(/-./g, (x) => x.toUpperCase()[1])
|
|
obj[cssInJsProp] = value.trim()
|
|
}
|
|
return obj
|
|
}
|
|
|
|
export const messageFormatStylesMap = {
|
|
black: 'color:#000000',
|
|
dark_blue: 'color:#0000AA',
|
|
dark_green: 'color:#00AA00',
|
|
dark_aqua: 'color:#00AAAA',
|
|
dark_red: 'color:#AA0000',
|
|
dark_purple: 'color:#AA00AA',
|
|
gold: 'color:#FFAA00',
|
|
gray: 'color:#AAAAAA',
|
|
dark_gray: 'color:#555555',
|
|
blue: 'color:#5555FF',
|
|
green: 'color:#55FF55',
|
|
aqua: 'color:#55FFFF',
|
|
red: 'color:#FF5555',
|
|
light_purple: 'color:#FF55FF',
|
|
yellow: 'color:#FFFF55',
|
|
white: 'color:#FFFFFF',
|
|
bold: 'font-weight:900',
|
|
strikethrough: 'text-decoration:line-through',
|
|
underlined: 'text-decoration:underline',
|
|
italic: 'font-style:italic',
|
|
obfuscated: 'filter:blur(2px)',
|
|
clickEvent: 'cursor:pointer',
|
|
}
|