# Legacy skin API

> The room.skin API of the legacy skin (basic) used by older chat rooms — jQuery DOM, menu bar, popups, nickname menu — and a mapping table for moving to the default skin.

The legacy skin (`basic`, shown in the admin page as "Legacy skin (old version)") is the skin older chat rooms use. New chat rooms are created with the default skin (`basic-v2`), and new features are added only to the default skin. This page lists `room.skin` for chat rooms that still use the legacy skin. For the default skin's API, see [Skin UI API](/en/doc/3.4).

Page script `U.chat()`, events, `room.action`, channels (`room.plugin`), and the server API are the same in both skins. Only `room.skin` differs.

## Check which skin is in use {#detect}

The members of `room.skin` appear only after the skin builds the screen, so check inside `after.create`.

```js
U.chat('my-room').on('after.create', function (room) {
	if (room.skin.menubar) {
		// Legacy skin
	} else if (room.skin.menu) {
		// Default skin
	}
});
```

Both skins have `userMenu`, so don't use it to tell them apart. You can change the skin in the admin page **Skin** menu or with the `<u-chat skin="basic-v2">` attribute. Features that don't depend on the skin, such as member integration, keep working after the change.

## Principles {#principles}

- The legacy skin works directly on the DOM inside the chat room iframe with jQuery (1.12). The skin's HTML structure and class names are not a fixed contract, and screens you modify may break when the skin changes.
- Do all registration inside `after.create`. Before that, only `room.skin.arguments` and `room.skin.messages` exist.
- Do not cover or remove the uchat logo or info display.

## room.skin members {#members}

| Name | Description |
| --- | --- |
| `room.skin.arguments` | Skin settings (admin page skin settings + `skin_arguments` attribute + global `Uchat_argument`). The skin fills in color defaults |
| `room.skin.messages` | Skin UI text (current language) |
| `room.skin.window` | The chat room iframe's `window`. jQuery is `room.skin.window.$` |
| `room.skin.document` | The chat room iframe's `document` |
| `room.skin.dom` | jQuery objects for the main areas. See [dom](#dom) below |
| `room.skin.menubar` | The menu bar under the input box. See [menubar](#menubar) below |
| `room.skin.popup` | Popups (alert, confirm, input, select). See [popup](#popup) below |
| `room.skin.userMenu` | The menu that opens when you click a nickname. See [userMenu](#user-menu) below |
| `room.skin.scrollDown(force)` | Scrolls the chat to the bottom. Unless `force` is true, it does not scroll while the user has scrolled up |

## dom {#dom}

All of these are jQuery objects for elements inside the chat room iframe.

| Key | Area |
| --- | --- |
| `wrap` | The whole chat room (`.wrap`) |
| `top` | Top title area (`.top`) |
| `middle` | Chat and user list area (`.middle`) |
| `content` | Chat scroll area (`.contentWrap`) |
| `content_target` | Element that chat lines are appended to (`.content`) |
| `userList` | User list scroll area (`.userListWrap`) |
| `contour` | Divider between the user list and the chat (`.contour`) |
| `bottom` | Bottom input area (`.bottom`) |
| `inputContent` | Input box (`.inputContent`) |
| `menubar` | Menu bar (`.menuBar`) |
| `latest_chat` | Latest-message notice shown when scrolled up (`.latest_chat`) |
| `setting`, `settingBG` | Settings window and its background (`.settingContent`, `.settingBackground`) |

```js
U.chat('my-room').on('after.create', function (room) {
	var $ = room.skin.window.$;
	var banner = $('<div/>').text('Event tonight at 8!').css({ padding: '6px', background: '#fff4cc' });
	room.skin.dom.middle.before(banner);
	$(room.skin.window).resize(); // If the height changed, let the skin re-layout
});
```

If you add elements and the height changes, call `$(room.skin.window).resize()` so the skin recalculates the chat area size. Insert user input with `.text()`, never `.html()`.

## menubar {#menubar}

Adds buttons to the menu bar under the input box.

```js
U.chat('my-room').on('after.create', function (room) {
	room.skin.menubar.add({
		id: 'hello',
		title: 'Send a greeting',
		html: '<div>Hi</div>',
		condition: function (room, item) {
			return room.my.auth >= 1; // Hidden when false
		},
		onClick: function (room, item) {
			room.action.send('Hello!');
		},
	});
});
```

| Key | Description |
| --- | --- |
| `id` | Required. Not registered if it's a duplicate. The button gets a `menubar-<id>` class |
| `title` | Tooltip on hover |
| `html` | Button content (HTML string). An empty `<div>` if omitted |
| `icon` | Background image URL |
| `condition(room, item)` | Not drawn if it returns exactly `false` |
| `onClick(room, item)` | On click. `item.event` is the click event |
| `beforeLoad(room, item)`, `afterLoad(room, item)` | Before and after drawing. Not drawn if `beforeLoad` returns `false` |
| `beforeUnload()`, `afterUnload()` | Before and after removal |

`item` is the object you passed to `add` itself, and the jQuery object of the drawn button is stored in `item.layout`.

| Function | Description |
| --- | --- |
| `add(item)` | Registers. Returns `true` on success, `false` if the ID is missing or a duplicate |
| `refresh()` | Draws registered buttons according to `condition` |
| `remove(id)` | Removes the registration |
| `hide()`, `show()`, `toggle()` | Hide, show, or toggle the whole menu bar |
| `list` | Array of registered items |

- **`add` alone does not show anything on screen.** The skin calls `refresh()` every time you join, so buttons registered in `after.create` appear on join. If you register after joining, call `room.skin.menubar.refresh()` yourself.
- `condition` is recalculated only on `refresh()`. If `condition` becomes `false` for a button already drawn, its registration is removed, and it does not come back even if the condition becomes true later. To hide and show a button by permission, `toggle()` the `item.layout` you get in `afterLoad` instead of using `condition`.
- `remove(id)` cannot remove the drawn button from the screen if the ID is not a number. When removing a button, also call `item.layout.remove()`.
- Default button IDs include `scroll`, `bold`, `picture`, and `call_admin`.

## popup {#popup}

Shows popups inside the chat room. Only one popup is visible at a time; the rest wait in order. Every function returns a popup number, and you can close it with `close(number)`. Results come through callbacks, not Promises.

```js
var popup = room.skin.popup;

popup.confirm('Rock paper scissors', 'Accept the challenge?', function (ok) {
	// true for OK, false for Cancel, null if closed by clicking outside
});

popup.prompt('Nickname', 'Enter a new nickname.', 'default', function (value) {
	// The text entered, null if canceled
});

popup.select('Choose', 'Pick one.', { r: 'Rock', p: 'Paper', s: 'Scissors' }, function (key) {
	// The chosen key (string), null if canceled
});

var id = popup.alert('Notice', 'Closes in 10 seconds.');
setTimeout(function () { popup.close(id); }, 10000);
```

| Function | Value passed to the callback |
| --- | --- |
| `alert(title, html, onOpen, onClose)` | `onOpen(layer, pb, pu)` is called **when it opens**, and `onClose()` when it closes. `html` is inserted as HTML, so escape user values |
| `confirm(title, subtitle, fn)` | `true`, `false`, or `null` if you click outside |
| `prompt(title, subtitle, value, fn)` | The text entered, `null` if canceled |
| `password(title, subtitle, value, fn)` | Same as `prompt`, with the input masked |
| `select(title, subtitle, list, fn)` | A key of `list` (object or array), `null` if canceled |
| `add(html, type, fn(layer, pb, pu), after)` | A custom popup. `layer` is `{ close(), centering() }`, `pb` is the background, `pu` is the popup body (jQuery). `after()` is called on close |
| `close([number])` | Closes that popup or removes it from the queue. Without a number, closes the popup currently shown |
| `closeByType(type)` | Closes all popups with the same `type` given to `add` |

- Pressing Enter in an input popup is the same as OK.
- When the browser window loses focus, the popup currently shown closes as if you clicked outside (`confirm`, `prompt`, and `select` return `null`). In flows where the user opens another window and comes back, write your code expecting the result may be `null`.

## userMenu {#user-menu}

Adds items to the menu that opens when you click a nickname. The menu is rebuilt on every click, so `condition` is recalculated each time.

```js
room.skin.userMenu.add({
	id: 'profile',
	text: 'View profile',
	condition: function (room, data) {
		return data.target !== room.my.nick; // Hidden on my own nickname
	},
	onClick: function (room, data) {
		var user = room.user.get(data.target);
		if (user && user.id) window.open('/profile/' + encodeURIComponent(user.id));
	},
});
```

| Key | Description |
| --- | --- |
| `id` | Required. Not registered if it's a duplicate (`false`) |
| `text` | Item text |
| `condition(room, data)` | Hidden if it returns exactly `false`. `data.target` is the clicked **nickname string** |
| `onClick(room, data)` | On click. `data` is `{ event, id, target, messageUid }` |

| Function | Description |
| --- | --- |
| `add(item)` | Adds one item |
| `group(left, right)` | Puts two items side by side on one row |
| `hr([{ condition }])` | Divider. Its ID is always `hr`, so `remove('hr')` removes all dividers |
| `remove(id)` | Removes all items with that ID (including default items) |
| `list` | Array of registered items |

Default item IDs include `info`, `whisper`, `call`, `ignore`, `un_ignore`, `report`, `invite`, `individual`, `mute`, `unmute`, `ban`, `ip`, `op`, `deop`, `freeze`, `unfreeze`, `clearlog`, `removeChat`, and `logout`. To hide a default item, call `remove(id)`. Even if you hide a menu item, the server still checks permissions.

## Legacy-only events {#legacy-events}

`before.user.list` and `after.user.list` (`data` is `{ list }`) fire when the legacy skin opens the user list and receives the full list. The default skin receives the user list in parts, so these events do not fire there. All other events are the same as in [Events](/en/doc/3.3).

## Moving to the default skin {#migrate}

| Task | Legacy skin | Default skin |
| --- | --- | --- |
| Add a button | `menubar.add({ id, title, html, condition(room, item), onClick(room, item) })` | `menu.add({ id, label, title, condition(ctx), onSelect(ctx) })` |
| Recalculate buttons | `menubar.refresh()` | `menu.resolver.refresh()` |
| Nickname menu | `userMenu.add({ id, text, onClick(room, data) })`, `data.target` is the nickname | `userMenu.add({ id, label, onSelect(ctx) })`, `ctx.target` is the user object |
| Hide default items | `userMenu.remove('ignore')` | Filter out `skin.ignore` with `userMenu.resolver.add(...)` |
| Popups | Callbacks, such as `popup.confirm(t, s, fn)` | Promises, such as `dialog.confirm(t, s)` |
| Insert an area | Direct DOM, such as `dom.middle.before(...)` | `mount.messageList({ position: 'before', mount(host) })` and others |
| Overlay on the chat | `dom.content.append(...)` + CSS | `mount.overlay({ layer, scope, mount(host) })` |
| Decorate nicknames and messages | Insert `<style>` via `window.$` | `nickname.resolver`, `message.resolver` |
| Scroll to bottom | `scrollDown(force)` | `scrollToBottom(force)` |

The default skin does not support skin DOM, jQuery, or inserting `<style>`. For how to work after moving, start with [Extending the skin UI](/en/doc/2.3).
