Events are how your app learns that something happened in chat: a new
message, someone typing, the connection dropping. You listen for the ones
your UI cares about and update the screen when they fire. This page lists
all 15 (from ChatEventMap in @livqeno/chat).
Where this fits: your app has connected with a chat token (see the Chat quickstart). Events only fire while connected. For what happened before that, load Message history.
These are client events, in your app. To tell your backend about chat activity (for push notifications, say), use Webhooks instead.
Which events does my UI need?
| UI piece | Listen for |
|---|---|
| Message list | message, messageUpdated, messageDeleted |
| Reaction counts | reactionAdded, reactionRemoved |
| "Bob is typing…" | typing |
| Online dots | presence |
| "Seen by" | read |
| "Reconnecting…" banner | connectionStateChanged (or reconnecting / reconnected) |
| Error toasts | error |
Listen to an event
In your app:
const unsubscribe = chat.on('message', (message) => {
render(message.senderId, message.text);
});
unsubscribe(); // idempotent — calling it twice is safechat.on()returns a function. Call it to stop listening, for example when the chat screen closes. Forgetting this leaks listeners, and you'll see each message rendered twice after the screen opens again.chat.off()also works, if you prefer it.- This is different from
room.on()in@livqeno/rtc, which returns the room.
Messages
| Event | Payload | Fires when |
|---|---|---|
message | message: ChatMessage | A message arrives, including your own (with its final id) |
messageUpdated | message: ChatMessage, info: { reason? } | A message was edited, or (reason: 'link_preview') its link preview arrived |
messageDeleted | event: MessageDeletedEvent | A message was deleted (soft delete) |
messagePinned / messageUnpinned | event: MessagePinEvent | A moderator pinned or unpinned a message |
outboxChanged | entries: OutboxEntry[] | A send was queued, went out, failed, or was saved. See Offline & sync |
outboxSent | { clientMessageId, room, message } | A queued send was saved. Fires just before the entry leaves the outbox, so an optimistic row can be swapped for message without flicker |
Why you get your own messages back:
sendMessage()finishing means the message is saved.- The
messageevent means it was delivered. - Draw messages from the event. Then everyone, including the sender, sees the same order.
Reactions
| Event | Payload | Fires when |
|---|---|---|
reactionAdded | event: ReactionEvent | Someone reacted |
reactionRemoved | event: ReactionEvent | A reaction was removed |
Adding the same reaction twice is harmless. See Reactions.
Typing, presence, and read receipts
These are short-lived signals about people, not about messages.
| Event | Payload | Fires when |
|---|---|---|
typing | event: TypingEvent | Someone started or stopped typing |
presence | event: PresenceEvent | Someone went online or offline |
read | event: ReadReceiptEvent | Someone read further |
presenceCount | event: { roomId, online } | Large rooms only: the online count, at most every 5 s. See Presence |
- None of these are saved, except the read position.
- A user goes offline when their presence expires. A crashed browser shows offline within 45 seconds.
- Don't rely on a
readevent for your own read marks on this device; use the value thatmarkAsRead()returns. Marks made on the same user's other devices do arrive asreadevents, andchat.inboxuses them to clear unread counts.
Moderation
| Event | Payload | Fires when |
|---|---|---|
memberBanned / memberUnbanned | event: MemberModerationEvent | A moderator banned or unbanned someone |
memberMuted / memberUnmuted | event: MemberModerationEvent | A moderator muted or unmuted someone |
removedFromRoom | event: { roomId, reason } | This user lost access to a room: reason: 'BANNED', or removed as a member. The SDK stops rejoining it |
See Moderation.
Connection
Phones lose signal and laptops sleep. These events let you show the user what's going on, while the SDK reconnects for you.
| Event | Payload | Fires when |
|---|---|---|
connectionStateChanged | state: ChatConnectionState | The connection state changed |
connected | — | connect() finished |
disconnected | — | The connection closed and the SDK won't retry: you called disconnect(), or retrying can't help (state failed) |
reconnecting | attempt: number | Trying to reconnect |
reconnected | — | Reconnected |
recovered | summary: RecoverySummary | Finished catching up after a reconnect |
resync | event: { roomId, page } | The client was away too long to catch up event by event, so history was re-read. Replace your copy with page |
What happens when the network drops:
- The state changes to
reconnecting, andreconnectingfires with attempt 1, 2, 3…. (disconnecteddoes not fire, because the SDK is still trying.) - When the network is back,
reconnectedfires. - The SDK fetches what was missed. Those messages arrive as normal
messageevents, followed by any missed edits, deletes, reactions and moderation events, through their normal events. recoveredfires when catch-up is done.
About recovered: missed messages already came through message. Then check:
summary.gap: some history was skipped. Reload the view.summary.errors: rooms that couldn't catch up. They retry on the next reconnect.summary.events: missed edits, deletes, reactions and moderation events applied.summary.resynced: rooms whose history was re-read (aresyncfired for each).
reconnecting gives the attempt number, so you can show "Still trying (3)".
RTC's version doesn't.
Errors
Listen for error in every app. A refused action (like a send without
permission) is reported here, not as a missing message.
| Event | Payload |
|---|---|
error | error: LivqenoChatError |
import { isLivqenoChatError } from '@livqeno/chat';
chat.on('error', (error) => {
if (!isLivqenoChatError(error)) return;
if (error.code === 'TOKEN_EXPIRED') refreshToken();
if (error.code === 'RATE_LIMITED') backOff(error.retryAfterSeconds);
});If a newer server sends an error code your client doesn't know, you still get
a LivqenoChatError with that code. Older clients keep working.
All codes are in Errors.
Error subclasses
Every chat error is really a LivqenoChatError underneath, so checking
instanceof LivqenoChatError (or calling isLivqenoChatError()) always
catches all of them, no matter which kind it is. If you want to handle just
one kind of error, use one of these more specific classes instead of
checking error.code yourself:
| Class | Default code | When it happens |
|---|---|---|
LivqenoChatConnectionError | CONNECTION_FAILED | The socket dropped, or never connected. |
LivqenoChatAuthenticationError | INVALID_TOKEN | The chat token is missing, badly formed, expired, or revoked. Mint a new one. |
LivqenoChatPermissionError | PERMISSION_DENIED | Your token is valid, but you're not allowed to do this — wrong scope, or you're not a member. |
LivqenoMessageError | INVALID_MESSAGE | Something was wrong with the message itself — too large, not found, or already deleted. |
LivqenoRoomError | ROOM_NOT_FOUND | The conversation doesn't exist, or you're not in it. |
LivqenoAttachmentError | ATTACHMENT_NOT_FOUND | Uploading, downloading, or looking up an attachment failed. |
There's also a second set of subclasses just for moderation —
LivqenoModerationError, LivqenoSlowModeError, LivqenoMessageBlockedError,
and LivqenoRateLimitError. See Moderation.
These map onto the wire
Each event comes from a WebSocket frame. For example, typing combines the
typing.started and typing.stopped frames. Building a client in another
language? See WebSocket protocol.
Other platforms
React gives you hooks that listen for you. Flutter gives you one stream per event type.
const { messages, send, loadMore, hasMore } = useMessages();
const { typingUsers, onInput } = useTyping();
const presence = usePresence();
const state = useChatConnectionState();
const error = useChatError();chat.messages.listen((m) { });
chat.messageUpdates.listen((m) { });
chat.messageDeletions.listen((id) { });
chat.typing.listen((e) { });
chat.presence.listen((p) { });
chat.reactions.listen((e) { });
chat.readReceipts.listen((r) { });
chat.connectionStateChanges.listen((s) { });
chat.errors.listen((e) { });Common mistakes
| Mistake | What you'll see | Fix |
|---|---|---|
Not calling unsubscribe() on cleanup | Messages render twice after reopening the chat. | Call the function on() returned. |
Only listening for message | A refused send just "disappears". | Listen for error too. |
| Adding your own message locally and from the event | Your messages show twice. | Only render from the message event. |
Next steps
- Messages · WebSocket protocol
- Event catalogue: every event across RTC, Chat, and webhooks.
- RTC events: the call-side equivalent.