Mixed runtime. The plugin definition (Documentation Index
Fetch the complete documentation index at: https://openturn.io/docs/llms.txt
Use this file to discover all available pages before exploring further.
./ entry) is worker-safe. The React widget (./react entry) is browser-only. State lives at G.plugins.chat once composed via withPlugins(baseDef, [chatPlugin]). Every seated player can send messages regardless of whose turn it is.
Install
Compose into your game
- A
G.plugins.chat: ChatSliceslice initialized to{ messages: [] }. - A namespaced
chat__sendmove dispatchable by every seated player. - Auto-merge of the chat slice into both
views.publicandviews.player(when declared) so clients receive history without extra wiring.
Server-side surface
chatPlugin
The plugin definition. Pass it to withPlugins. Plugin id is "chat" (CHAT_PLUGIN_ID).
chat__send move
- Empty / whitespace-only
textrejects withreason: "empty_message". textis trimmed and capped toMAX_MESSAGE_LENGTH(500 chars).displayNameis trimmed and capped toMAX_DISPLAY_NAME_LENGTH(40 chars). Empty falls back toPlayer {playerID}.- History is capped to
MAX_HISTORY(200 messages); older entries fall off the front.
G.plugins.chat.messages as:
Constants
| Name | Value |
|---|---|
CHAT_PLUGIN_ID | "chat" |
MAX_MESSAGE_LENGTH | 500 |
MAX_DISPLAY_NAME_LENGTH | 40 |
MAX_HISTORY | 200 |
Types
ChatMessage—{ authorPlayerID, authorDisplayName, text }.ChatSlice—{ messages: readonly ChatMessage[] }.ChatSendArgs—{ text: string; displayName: string }.ChatPluginEvent—"chat__send".
React widget — ./react
<ChatBubble /> renders a floating chat bubble in the bottom-right corner via a portal. It auto-hides until the match is live (when room.game is null, e.g. in lobby), so you can mount it once outside any lobby/game branch and let it persist across the transition.
Props
userID, userName, and game.{ snapshot, dispatch, playerID }. It does not import @openturn/react directly, so the package stays free of UI plumbing dependencies.
Display name resolution
The bubble picks the first available, non-generic value:- Explicit
displayNameprop. room.userName.Player {userID slice}fromroom.userID.Player {playerID}(orSpectatorwhen no seat).
"anonymous" are skipped.
Behavior
- Sending: Enter to submit (Shift+Enter is reserved for IME composition; the input is intentionally not a
<form>to survive sandboxed iframes that omitallow-forms). - Spectators (
playerID === null) can read but not send. - Unread badge: messages received while the panel is closed bump a counter; opening the panel marks them seen.
- Auto-scroll on new messages; auto-focus the input on open.
- Server rejections surface as inline error text under the composer (
humanizeChatErrormapsempty_message,inactive_player,not_connectedto user-friendly copy).
Styling
The widget uses inline styles only — no Tailwind, CSS modules, or stylesheet imports. This keeps the package drop-in for any host shell. The portal mounts ondocument.body by default with a high z-index (2147483000) so it floats above app chrome.
Example
examples/plugins/tic-tac-toe-with-chat — the canonical end-to-end example. Game is composed with withPlugins; the app mounts <ChatBubble /> outside the HostedRoom switch so it persists across the lobby → game transition.
See also
@openturn/plugins— the composition surface (withPlugins,definePlugin).- Reference:
@openturn/react—createOpenturnBindingsanduseRoom.