Search documentation

Search pages and headings

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:

  1. kind === 'text' — render text as a text node.
  2. kind === 'emoji' and is_custom === true — render an image. Take the last entry of thumbnails (the array is ascending by size) and use its url.
  3. kind === 'emoji' and is_custom === false — render a character: use mapped_unicode when it is non-empty, otherwise emoji_id if YouTube sent a raw emoji character there, otherwise fall back to shortcut.

thumbnails can be populated even when is_custom is false — YouTube ships images for some standard emoji too. is_custom is 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;
}