Message Model
Rendering Message Bodies
When to use message versus parts, and the three rules for drawing a part correctly.
Every message carries its body twice.
| Field | Use it for |
|---|---|
message |
Logs, notifications, search indexes, anything plain-text |
parts |
Chat UI — it is the only form that can render custom emoji as images |
In message, every emoji has been flattened to its shortcut, so a message
reading hello :crown: in plain text is hello 👑 when drawn from parts.
Rendering a chat UI from message means showing users literal :crown:
strings.
The three rules
For each entry in parts:
kind === 'text'— rendertextas a text node.kind === 'emoji'andis_custom === true— render an image. Take the last entry ofthumbnails(the array is ascending by size) and use itsurl.kind === 'emoji'andis_custom === false— render a character: usemapped_unicodewhen it is non-empty, otherwiseemoji_idif YouTube sent a raw emoji character there, otherwise fall back toshortcut.
thumbnailscan be populated even whenis_customisfalse— YouTube ships images for some standard emoji too.is_customis the deciding flag for image versus character, not the presence of thumbnails.
React
PartSchema is exported by the client package.
import type { PartSchema } from '@gettersethya/yt-livechat-client'
function MessageBody(props: { parts: readonly PartSchema[]; message: string }) {
if (props.parts.length === 0) return <>{props.message}</>
return (
<>
{props.parts.map((part, index) => {
if (part.kind === 'text') {
return <span key={index}>{part.text}</span>
}
if (part.is_custom) {
const source = part.thumbnails[part.thumbnails.length - 1]?.url
return source === undefined ? (
<span key={index}>{part.shortcut}</span>
) : (
<img key={index} src={source} alt={part.shortcut} className="emoji" />
)
}
const unicode = part.mapped_unicode !== '' ? part.mapped_unicode : part.emoji_id
return <span key={index}>{unicode !== '' ? unicode : part.shortcut}</span>
})}
</>
)
}Always guard the thumbnail lookup — an emoji flagged custom with an empty
thumbnails array would otherwise render a broken image.
Plain text and terminals
When you cannot draw images, degrade custom emoji to their shortcut:
import type { PartSchema } from '@gettersethya/yt-livechat-client'
function bodyForConsole(parts: readonly PartSchema[]): string {
return parts
.map((part) => {
if (part.kind === 'text') return part.text
if (part.is_custom) {
const source = part.thumbnails[part.thumbnails.length - 1]?.url
return source !== undefined ? `[img ${source}]` : part.shortcut
}
if (part.mapped_unicode !== '') return part.mapped_unicode
return part.emoji_id !== '' ? part.emoji_id : part.shortcut
})
.join('')
}
client.on('message', (message) => {
console.log(`${message.author}: ${bodyForConsole(message.parts)}`)
})For most logging, message.message already does this and is simpler.
Why mapped_unicode exists
YouTube's standard emoji arrive with ids like face-blue-smiling rather than a
character. The server maps the known ones to real unicode, so
mapped_unicode is 😊 where a mapping exists and "" where it does not —
which is exactly why rule 3 has two fallbacks behind it.
Empty bodies
parts is empty for events with no textual body — most ticker items, and
some engagement notices. Fall back to message (as the React example does),
and be ready for both to be empty.
Sizing emoji
Custom emoji thumbnails are frequently larger than the surrounding line. Pin them to the text size rather than shipping them at natural dimensions:
.emoji {
display: inline-block;
height: 1.25em;
width: auto;
vertical-align: text-bottom;
}