Events
A list of chat room events, the data each one receives, and what is actually blocked when a before handler returns false.
Last updated October 20, 2026
On this page
You receive what happens in a chat room with U.chat(room).on(name, function (room, data) { ... }). This page lists, based on the code, when each event fires, what data contains, and what is actually blocked when a before. handler returns false. For how to register and remove handlers, see U.chat() and the room object.
Rules
- Most events come in
before.nameandafter.namepairs.before.fires before the default handling,after.fires after it. - Callbacks receive
(room, data).roomis the room object. - If a
before.callback returns exactlyfalse, the callbacks after it are not called, and only the handling listed in the "Ifbeforereturns false" column below stops. What gets blocked differs per event. - If you change values in
datainsidebefore., the handling that follows (display, sending) uses the changed values. The table notes which events you can modify. - When the default handling is blocked,
after.does not fire either.
Event list
| Event | When | data |
If before returns false |
|---|---|---|---|
before.create |
Room installation starts (before settings and skin are loaded). Fires again on every reinstall | {} |
Cannot be blocked (installation continues) |
after.create |
Room screen is ready (before connecting). Fires again on every reinstall (room.reload()) |
{ id } |
— |
before.join |
Right before the join request is sent to the server. Fires again on every reconnect | { user } |
Rooms without member integration: the join request is not sent. Rooms with member integration: see the warning below |
after.join |
Join completed and your info received. Fires again on every reconnect | { my } |
(no before) |
before.message, after.message |
One chat message line (including past messages and your own messages) | Message | Not shown on screen |
before.whisper, after.whisper |
A whisper (including the echo of whispers you sent) | Whisper | Not shown on screen |
before.send, after.send |
When you send a message (excluding /command and !command input) |
{ message, bold, italic, underline, color } |
Not sent. room.action.send() returns false |
before.command, after.command |
When a command runs | { type, data } |
Not run. room.action.command() returns false |
before.plugin, after.plugin |
When channel data arrives, or when you enter a !command |
Channel | Received data: onReceived callbacks are not called. !command: addParser callbacks are not called and room.action.plugin() returns false |
before.user.join, after.user.join |
Someone else joins. For cases where it does not fire, see User changes | { user } |
The join notice is not shown. The user is already in room.user.list |
before.user.quit, after.user.quit |
Someone else leaves | { user: { nick } } |
The leave notice is not shown and the user is not removed from room.user.list |
before.user.modi, after.user.modi |
Someone's (including your own) nickname, role, or mute changes | { target, changed } |
Not applied to room.my, room.user.list, or the screen. room.user.get() may already return the new value. We recommend not blocking this |
before.user.count, after.user.count |
Only the user count changes (for when this fires instead of join/leave, see User changes) | { count } |
The user count on screen is not updated |
before.update, after.update |
Room settings change | Changed values { key: value } |
Not applied to room.info or the screen |
before.system, after.system |
System notice lines (notices, bans, mutes, calls, etc., including room.print) |
{ type, data } |
Not shown on screen |
before.control, after.control |
Server control signals (clearing the chat, vote status, etc.) | { type, data } |
Not passed to the skin |
before.error, after.error |
Errors (server errors and client-side errors such as command argument checks) | { code } |
The error is not shown on screen |
before.disconnect, after.disconnect |
The connection drops (does not fire when leaving the page or removing the tag) | {} |
The skin is not notified, and automatic reconnection after a network error does not happen |
Blocking in before. does not undo what happened on the server. For example, hiding a ban notice with before.system does not stop the ban, and blocking before.user.quit does not keep that person in the room. In most cases, only what appears on your own screen changes.
Order of events
When the room opens
before.create— installation starts.room.infoandroom.myare still empty.- Room settings, language, plugins, and skin are loaded (plugin scripts run here).
after.create— the skin screen is ready. Register skin UI and channels here.before.join→ join request to the server →after.join.- Past messages and notices arrive as
message,whisper, andsystemevents.
In rooms that show a join check (bot check) screen, step 1 starts after the check passes. If you reconnect because of a network problem, events start again from step 4.
When you send a message
before.send→ sent to the server →after.send- The server sends the message to everyone in the room (including you).
before.message→ shown on screen →after.message
after.send only means "sent to the server", not that it was accepted. If the server rejects it, the message does not come back; instead you get an error event (no permission 410, repeated content 430, etc.), a system event (muted, bad_word), or a control event (slowWait). Use after.message to confirm that the message actually went through.
If the chat input starts with /, it goes to the command event instead of send; if it starts with !, it goes to the plugin event. The default skin sends a !command as a normal message when no channel handles it or it is blocked.
When the connection drops
before.disconnect→ skin notified → automatic reconnect decided →after.disconnect- If the drop was caused by a network error, the client reconnects and events start again from
before.join.
If you were banned, or the connection dropped right after the server sent an error, the client does not reconnect. The disconnect event does not fire when the browser is closed, the user navigates to another page, the <u-chat> tag is removed, or room.reload() is called. Do anything that must happen before leaving in the browser's pagehide event.
Join
U.chat('my-room').on('after.join', function (room, data) {
// data.my is a user object with the same shape as room.my
room.print('Welcome, ' + data.my.nick + '.');
});
before.create fires when there is no screen and no info yet, so you rarely need it. It cannot be blocked; to change join info, use before.join.
data.user in before.join:
| Room | data.user |
What you can change |
|---|---|---|
| Room without member integration | { nick } — the nickname saved in this browser (empty if none) |
Changing data.user.nick requests the join with that nickname |
| Room with member integration | The decoded user_data (or user_data_url): the keys you put in user_data, plus time and hash |
Do not change it. The signature will not match and you get error 202 |
Warning: In a room with member integration, returning
falsefrombefore.joindoes not block the join. Instead, the request is sent with empty join info and ends in an error. Do not usebefore.jointo block joining in rooms with member integration. To keep someone out, leave outuser_dataor do not add the tag.
Message
U.chat('*').on('after.message', function (room, data) {
console.log(data.user.nick, data.content);
});
| Key | Description |
|---|---|
user |
The sender's user object |
content |
Content |
style |
{ bold, italic, underline, size, color }. If color is missing, the room's default text color applies |
created |
Time sent, 'YYYY-MM-DD HH:MM:SS' (in the viewer's time zone) |
time |
Time sent (Unix seconds) |
uid |
Unique message ID (hex string). Empty if none |
past |
true for past chat received again when joining or reconnecting. Absent on newly posted messages |
- If you change
data.contentinbefore.message, the changed content is shown only on your screen. - Messages from people you ignore (below moderator) reach
before.messageonly; they are not shown andafter.messagedoes not fire.
Whisper
| Key | Description |
|---|---|
from, to |
The sender's and recipient's user objects |
content |
Content |
style |
Currently always an empty string. Its shape differs from the message style object |
created, time |
Time sent (string, Unix seconds) |
- The server sends a whisper to both the sender and the recipient, so whispers you send also come back through this event. If
data.from.nickis your nickname, you sent it. - Unlike messages, there is no
uid. - When you join, whispers involving you arrive again along with past messages.
- If the whisper is from someone you ignore (below moderator), or your settings refuse whispers, it reaches
before.whisperonly; it is not shown andafter.whisperdoes not fire.
Sending and commands
// Append a signature to outgoing messages
U.chat('*').on('before.send#sign', function (room, data) {
data.message += ' ~';
});
// Block the whisper command
U.chat('*').on('before.command#no-whisper', function (room, data) {
if (data.type === 'whisper') return false;
});
- If you change
message,bold,italic,underline, orcolorinbefore.send, the changed values are sent. - The
typeinbefore.commandis the name in lowercase with_removed (un_ignore→unignore). If you changedata.data(the argument array), the command runs with the changed arguments.
Channel
before.plugin/after.plugin fire in two cases, with a different data shape for each.
| Case | data |
|---|---|
| Channel data sent by someone else or the server arrives | { user, type, data } — user is the sender, type is 0 (to everyone) or 1 (to me only), data is the value array |
!name args entered in the chat input (or room.action.plugin()) |
{ type: 'name', data: ['arg', ...] } — type is the channel name |
Data sent by the server (REST API) and channel saved values received again on join have user.auth set to 4 and an empty nickname. Check whether data came from the server only with data.user.auth === 4.
Received data does not include the channel name. To receive data per channel, use room.plugin.add(channel).onReceived. See Channel API and limits.
User changes
U.chat('*').on('after.user.modi', function (room, data) {
// data.target nickname before the change
// data.changed whichever of { nick?, auth?, mute? } changed
if (data.changed.nick) room.print(data.target + ' → ' + data.changed.nick);
});
- Fires only for changes after joining. It also fires when your own nickname, role, or mute changes, and
room.mychanges too. For the role at join time, look atdata.userinuser.joinordata.myinafter.join. - Inside
before.user.modi,room.myandroom.user.listhave not changed yet.room.user.get()may already return the new value. - In some cases
user.joinanduser.quitdo not fire, and onlyuser.count(every 3 seconds) does:- The default skin. This is always the case, whether the user list is open or closed.
- The legacy skin while the user list is closed
- Rooms with 100 or more users at the time you join
- In these cases, joins and leaves are reflected only in the user list on screen; there is no event telling the page script who joined or left.
room.user.listis not updated either. The only thing the page script can rely on is the user count (user.count). - Build features such as join/leave notifications on the assumption that they only work in small rooms using the legacy skin with the user list open. If your server needs to know, use the user count from the Server REST API.
- The
user.listevent ({ list }) fires only in legacy skins. The default skin receives the user list in parts, so this event does not fire.
System notices
data.type is the notice type and data.data is the value array. The shape of the value array differs by type.
type |
Meaning |
|---|---|
notice |
Notice, room.print() |
ban |
Ban |
mute, permanent_mute |
Mute (timed, permanent) |
un_mute |
Mute lifted |
to_call, from_call |
Call sent, received |
invite, invite_notice |
Private channel invite received, sent |
ignore, un_ignore |
Ignore, unignore |
frozen, unfrozen |
Room frozen, unfrozen |
op, deop |
Moderator assigned, removed |
user_info |
Result of the info command |
ip |
Result of the ip command |
admin_login, admin_logout |
Someone became admin with /login, stepped down |
clear_log |
Chat cleared |
muted |
Your message was rejected because you are muted |
bad_word |
Your message was rejected by the word filter |
retry_connect |
Reconnect attempt (number of attempts) |
U.chat('*').on('after.system', function (room, data) {
if (data.type === 'from_call') window.focus();
});
For users who have turned off receiving calls or invites, even before.system for from_call and invite does not fire.
Control signals
data.type is the signal name and data.data is the value array. Most of these are internal signals used by the skin, so names and shapes may change. The ones useful in page scripts are roughly these:
type |
Meaning | data |
|---|---|---|
clearLog |
Chat cleared | [] |
removeChat |
An admin deleted messages | List of uids of deleted messages. Match them against the uid of message |
slowWait |
Your message was rejected because of slow mode | [remaining milliseconds, content you tried to send] |
pluginDataReplaced |
A channel saved value was replaced in a free room | []. See Channel API and limits |
Errors
data.code is the error number, and the message in the current language is in room.language.error[code].
U.chat('*').on('after.error', function (room, data) {
console.warn(data.code, room.language.error[data.code]);
});
| Code | Meaning |
|---|---|
101 |
Protocol error (invalid value, channel saved value too large, etc.) |
201 |
Room does not exist |
301 |
Nickname already in use |
202 |
Member integration signature does not match |
203 |
Connected to a member integration room without login info |
205 |
Login info older than the time sync limit, or the same user_data more than 5 minutes after its first use |
207 |
Room is full |
208, 701 |
Wrong password, password required |
402 |
No permission |
404 |
Target not found |
407 |
Command cannot be used on yourself |
410 |
No chat permission (feature condition) |
412 |
Invalid command arguments |
415 |
You are banned |
416 |
Command not available in this room |
421 |
Already has that role |
422 |
Wrong password |
413 |
You are muted |
418 |
Room is frozen |
424 |
Cannot send because you are refusing whispers |
425 |
Sending too often |
428 |
Channel sending limit exceeded |
429 |
Too many login attempts |
430 |
Sending the same content repeatedly |
501, 502 |
No private channel; cannot create a private channel inside a private channel |
For causes and fixes of member integration errors, see Member integration.
data examples
These are the actual shapes of data for each event. Values are examples, and keys the server did not send may be undefined. To check for yourself, log it to the console, for example U.chat('*').on('after.message', function (room, data) { console.log(data); }).
// after.join
{ my: { nick: '홍길동', id: 'user1', level: 5, auth: 1, icons: '', nickcon: '',
connected: '2026-10-17 12:00:00', time: 1792209600, mute: 0, session: 'a1b2c3d4e5f6', profileimg: '' } }
// before.join (room without member integration)
{ user: { nick: '홍길동' } }
// after.message
{ user: { nick: '홍길동', auth: 1, ... },
content: 'Hello',
style: { bold: false, italic: false, underline: false, size: '9pt', color: '#000000' },
created: '2026-10-17 12:00:01', time: 1792209601, uid: '0a1b2c3d4e' }
// after.whisper
{ from: { nick: '홍길동', ... }, to: { nick: '김철수', ... }, style: '', content: 'One moment',
created: '2026-10-17 12:00:02', time: 1792209602 }
// before.send
{ message: 'Hello', bold: undefined, italic: undefined, underline: undefined, color: undefined }
// before.command (/w 김철수 One moment)
{ type: 'whisper', data: ['김철수', 'One moment'] }
// after.user.join / after.user.quit
{ user: { nick: '김철수', auth: 0, ... } }
{ user: { nick: '김철수' } }
// after.user.modi
{ target: '김철수', changed: { nick: '철수' } }
{ target: '김철수', changed: { auth: 2 } }
{ target: '김철수', changed: { mute: 1 } }
// after.user.count
{ count: 152 }
// after.update
{ slowSeconds: 5 }
// after.system
{ type: 'mute', data: ['Admin', '김철수', 10, 'Spamming'] } // admin, target, minutes, reason
{ type: 'from_call', data: ['김철수', '홍길동'] } // sender, recipient
// after.control
{ type: 'removeChat', data: ['0a1b2c3d4e'] }
// after.plugin (channel data received / !dice 1 6 entered)
{ user: { nick: '김철수', auth: 1, ... }, type: 0, data: [4] }
{ type: 'dice', data: ['1', '6'] }
// after.error
{ code: 402 }
Still not sure how it works? Ask your question on the contact board.