---
source_url: https://www.pubnub.com/docs/chat/chat-sdk/migrate/js-chat-v10-migration-guide
title: JavaScript Chat SDK 1.0.0 migration guide
updated_at: 2026-06-30T11:04:33.331Z
---

> Documentation Index
> For a curated overview of PubNub documentation, see: https://www.pubnub.com/docs/llms.txt
> For the full list of all documentation pages, see: https://www.pubnub.com/docs/llms-full.txt


# JavaScript Chat SDK 1.0.0 migration guide

The 1.0.0 release of the JavaScript Chat SDK introduces a set of API improvements that make real-time subscriptions more discoverable, event payloads strongly typed, and membership management more expressive. The core change is the shift from generic listener methods to entity-owned callbacks: instead of calling `chat.listenForEvents()` or passing a callback to `channel.connect()`, you now attach callbacks directly to entity objects (`channel.onMessageReceived`, `user.onMentioned`, etc.).

This guide is for developers who use JavaScript Chat SDK `0.x.x` in existing applications.

If your application uses `0.x.x`, it will not compile against `1.0.0` without changes to `join()`, `Message.reactions`, and `GetCurrentUserMentionsResult`. This guide summarizes the differences between the versions and shows how to migrate to JavaScript Chat SDK `1.0.0`.

Most notable changes include:

* **join() signature change**: The callback parameter is removed and `join()` no longer subscribes to messages automatically. Call `onMessageReceived()` separately after joining to start message delivery. `join()` now returns `Promise<Membership>` directly instead of a `JoinResult` wrapper.
* **Entity-first callbacks**: All streaming methods gain entity-owned alternatives. `connect()` → `onMessageReceived()`, `getTyping()` → `onTypingChanged()`, `streamPresence()` → `onPresenceChanged()`, `streamUpdates()` → `onUpdated()` + `onDeleted()`. The old methods are deprecated but still compile.
* **Typed event payloads**: Events that previously required manual `EventContent` parsing now deliver purpose-built types. `onMentioned()` delivers `Mention`, `onInvited()` delivers `Invite`, `onRestrictionChanged()` delivers a restriction object, and `onReadReceiptReceived()` delivers `ReadReceipt`.
* **Message.reactions type change**: `Message.reactions` changed from an object of reaction lists to `MessageReaction[]`. Each `MessageReaction` exposes `value`, `isMine`, and `userIds`.
* **GetCurrentUserMentionsResult.mentions**: The `enhancedMentionsData` field is deprecated (still available) and a new `mentions` field is added. Its element type changes to a unified `UserMention` type with fields `message` (a `Message` object), `userId`, `channelId`, and `parentChannelId?`.
* **sendText() renamed**: The old multi-parameter `sendText()` overload is renamed to `sendTextLegacy()`. Use the new `sendText(text, options?: SendTextParams)` form.
* **MessageDraft is now the primary draft class**: What was `MessageDraftV2` is now `MessageDraft`. The old `MessageDraft` class is renamed to `MessageDraftV1` and deprecated. Use `channel.createMessageDraft()` — it now returns the new `MessageDraft`.
* **New read receipt capabilities**: `channel.fetchReadReceipts()` returns a paginated snapshot of read positions. A new `emitReadReceiptEvents` configuration option controls which channel types emit read receipt signals.
* **New membership methods**: `channel.hasMember()`, `channel.getMember()`, `user.isMemberOf()`, and `user.getMembership()` let you check and retrieve a specific membership without fetching the full list.

## Differences between 0.x.x and 1.0.0

| Feature/Method | `0.x.x` | `1.0.0` |
| --- | --- | --- |
| `join()` return type | `Promise<JoinResult>` (includes disconnect) | `Promise<Membership>` |
| `join()` signature | `join(custom?, callback?)` | `join(params?: { status?, type?, custom? })` |
| `JoinResult` | Available | Removed |
| Message delivery after `join()` | Automatic (via callback) | Requires explicit `onMessageReceived()` call |
| Disconnect from message delivery | `joinResult.disconnect()` (function) | `disconnect()` returned by `onMessageReceived()` |
| `connect()` | Primary method for message delivery | Deprecated — use `channel.onMessageReceived()` |
| `getTyping()` | Typing indicator streaming | Deprecated — use `channel.onTypingChanged()` |
| `streamPresence()` | Presence streaming | Deprecated — use `channel.onPresenceChanged()` |
| `streamUpdates()` on entity | Update streaming (single entity) | Deprecated — use `onUpdated()` |
| `streamUpdatesOn()` on entity | Update streaming (multiple entities) | Still supported |
| Deletion events | Part of `streamUpdates()` callback | Separate `onDeleted()` event |
| `streamReadReceipts()` | `{ [timetoken: string]: string[] }` callback | Deprecated — use `channel.onReadReceiptReceived()` with typed `ReadReceipt` |
| `streamMessageReports()` | Raw event callback | Deprecated — use `channel.onMessageReported()` |
| `Message.reactions` type | Object of reaction lists | `MessageReaction[]` |
| `Message.actions` | Available | Deprecated |
| Mention events | `chat.listenForEvents({ type: "mention" })` | `user.onMentioned(Mention)` |
| Invite events | `chat.listenForEvents({ type: "invite" })` | `user.onInvited(Invite)` |
| Moderation events | `chat.listenForEvents({ type: "moderation" })` | `user.onRestrictionChanged({ userId, channelId, ban, mute, reason? })` |
| Custom events (emit) | `chat.emitEvent()` | Deprecated — use `channel.emitCustomEvent()` |
| Custom events (listen) | `chat.listenForEvents({ type: "custom" })` | Deprecated — use `channel.onCustomEvent()` |
| `EventContent.*` types | Available | Deprecated |
| Old `sendText()` overload | `sendText(text, options)` with legacy params | Renamed to `sendTextLegacy()` |
| New `sendText()` overload | Not available | `sendText(text, options?: SendTextParams)` |
| Primary draft class | `MessageDraftV2` (via `createMessageDraftV2()`) | `MessageDraft` (via `createMessageDraft()`) |
| Old draft class | `MessageDraft` (via `createMessageDraft()`) | `MessageDraftV1` (via `createMessageDraftV1()`), deprecated |
| Read receipts snapshot | Not available | `channel.fetchReadReceipts()` → `ReadReceiptsResponse` |
| Read receipt timetokens | N/A | `string` (not `number`) |
| `emitReadReceiptEvents` config | Not available | Available in `ChatConfig` |
| `GetCurrentUserMentionsResult` field | `enhancedMentionsData` | `mentions: UserMention[]` |
| Membership existence check | `getMembers()` then filter | `channel.hasMember(userId)`, `channel.getMember(userId)` |
| User membership check | `getMemberships()` then filter | `user.isMemberOf(channelId)`, `user.getMembership(channelId)` |
| `removeThread()` return | Composite object | `Promise<boolean>` |
| `createThread()` | No arguments, returns `ThreadChannel` | Required `text` parameter, returns `CreateThreadResult` |
| `Channel.delete()` / `User.delete()` | Accepts `soft` parameter | `soft` parameter removed; returns `Promise<true>` |
| `Membership.update()` signature | `update({ custom })` | `update({ status?, type?, custom? })` |

## join() and onMessageReceived()

:::warning Important change
This change affects any code that calls `join()` and then listens for messages. Update it before releasing to users.
:::

In `0.x.x`, calling `join()` on a channel both created the membership server-side and subscribed to message delivery automatically — messages arrived via the callback you passed to `join()`, and the returned `disconnect` function stopped the subscription. In `1.0.0`, `join()` creates the membership only. Call `onMessageReceived()` separately to start receiving messages.

The `JoinResult` type is removed. `join()` now returns `Promise<Membership>` directly. The disconnect function is returned by `onMessageReceived()`.

###### 0.x.x

```typescript
// join() both created the membership and started message delivery
const { membership, disconnect } = await channel.join(
  (message) => {
    console.log("New message:", message.text);
  },
  { role: "member" }
);

// Stop message delivery:
disconnect();
await channel.leave();
```

###### 1.0.0

```typescript
// join() creates the membership — messages are not delivered automatically
const membership = await channel.join({
  status: "active",
  custom: { role: "member" },
});

// Subscribe to messages separately
const disconnect = channel.onMessageReceived((message) => {
  console.log("New message:", message.text);
});

// Stop message delivery and leave:
disconnect();
await channel.leave();
```

## Entity-first callbacks

In `0.x.x`, streaming subscriptions were created by calling methods on the `Chat` or `Channel` object and passing a callback as a parameter. In `1.0.0`, entity objects expose dedicated callback methods. Pass a handler function and receive a disconnect function in return — call it to stop the subscription.

The old instance methods (`connect()`, `getTyping()`, `streamPresence()`, `streamUpdates()`) are deprecated but still compile. The static `streamUpdatesOn()` helper remains supported. Migrate to the new names at your own pace.

### 0.x.x

```typescript
// Channel — message delivery
const stopMessages = channel.connect((message) => {
  console.log("Message:", message.text);
});

// Channel — typing indicator
const stopTyping = channel.getTyping((typingUserIds) => {
  console.log("Typing:", typingUserIds);
});

// Channel — presence
const stopPresence = channel.streamPresence((presence) => {
  console.log("Present:", presence.occupants);
});

// Channel — metadata updates
const stopUpdates = channel.streamUpdates((updatedChannel) => {
  console.log("Channel updated:", updatedChannel?.id);
});

// User — metadata updates
const stopUserUpdates = user.streamUpdates((updatedUser) => {
  console.log("User updated:", updatedUser?.id);
});

// Membership — updates
const stopMembershipUpdates = membership.streamUpdates((updated) => {
  console.log("Membership updated");
});
```

### 1.0.0

```typescript
// Channel — message delivery
const stopMessages = channel.onMessageReceived((message) => {
  console.log("Message:", message.text);
});

// Channel — typing indicator
const stopTyping = channel.onTypingChanged((typingUserIds) => {
  console.log("Typing:", typingUserIds);
});

// Channel — presence
const stopPresence = channel.onPresenceChanged((userIds) => {
  console.log("Present:", userIds);
});

// Channel — metadata updates (update and deletion are now separate)
const stopUpdated = channel.onUpdated((updatedChannel) => {
  console.log("Channel updated:", updatedChannel.id);
});
const stopDeleted = channel.onDeleted(() => {
  console.log("Channel was deleted");
});

// User — metadata updates
const stopUserUpdated = user.onUpdated((updatedUser) => {
  console.log("User updated:", updatedUser.id);
});
const stopUserDeleted = user.onDeleted(() => {
  console.log("User was deleted");
});

// Membership — updates
const stopMembershipUpdated = membership.onUpdated((updated) => {
  console.log("Membership updated");
});
const stopMembershipDeleted = membership.onDeleted(() => {
  console.log("Membership was deleted");
});

// All return () => void — call to unsubscribe
stopMessages();
```

The same pattern applies to `message.onUpdated()` for individual message update subscriptions.

## Typed event payloads

In `0.x.x`, invite, mention, and moderation events required calling `chat.listenForEvents()` with a type string and reading fields from the raw event payload. In `1.0.0`, these events are exposed as dedicated entity callbacks with purpose-built types. The `chat.listenForEvents()` method and all `EventContent` types are deprecated.

### 0.x.x

```typescript
// Mention events — raw event with untyped payload
const stopMentions = chat.listenForEvents({
  channel: currentUser.id,
  type: "mention",
  callback: (event) => {
    console.log("Mentioned in:", event.payload.channelId);
  },
});

// Invite events
const stopInvites = chat.listenForEvents({
  channel: currentUser.id,
  type: "invite",
  callback: (event) => {
    console.log("Invited to:", event.payload.channelId);
  },
});

// Moderation events
const stopModeration = chat.listenForEvents({
  channel: `PUBNUB_INTERNAL_MODERATION.${currentUser.id}`,
  type: "moderation",
  callback: (event) => {
    console.log("Restricted on:", event.payload.channelId);
  },
});

// Message report events
const stopReports = channel.streamMessageReports((event) => {
  console.log("Reported:", event.payload.reason);
});
```

### 1.0.0

```typescript
// Mention events — typed Mention
const stopMentions = user.onMentioned((mention: Mention) => {
  console.log("Mentioned in:", mention.channelId);
  console.log("By:", mention.mentionedByUserId);
  console.log("Message timetoken:", mention.messageTimetoken);
});

// Invite events — typed Invite
const stopInvites = user.onInvited((invite: Invite) => {
  console.log("Invited to:", invite.channelId, "(", invite.channelType, ")");
  console.log("By:", invite.invitedByUserId, "at:", invite.invitationTimetoken);
});

// Moderation events — restriction object { userId, channelId, ban, mute, reason? }
const stopModeration = user.onRestrictionChanged((restriction) => {
  console.log("Channel:", restriction.channelId);
  console.log("Ban:", restriction.ban, "Mute:", restriction.mute);
  console.log("Reason:", restriction.reason);
});

// Message report events — typed MessageReport
const stopReports = channel.onMessageReported((report: MessageReport) => {
  console.log("Reported by:", report.reportedUserId, "reason:", report.reason);
});
```

## Read receipts

The read receipts API gains two new capabilities in `1.0.0`: a typed real-time callback (`onReadReceiptReceived()`) and an on-demand snapshot (`fetchReadReceipts()`). The old `streamReadReceipts()` method is deprecated.

In `0.x.x`, `streamReadReceipts()` delivered an object on every event, where keys were timetokens and values were arrays of user IDs who had read up to that point.

In `1.0.0`, `onReadReceiptReceived()` fires once per incoming receipt signal and delivers a single `ReadReceipt` value containing the user ID and their last-read timetoken as a `string`. `fetchReadReceipts()` provides a paginated snapshot of the current read state for all channel members.

:::note
Read receipt timetokens are `string` values in the JavaScript SDK, unlike the `Long` type in Kotlin.
:::

###### 0.x.x

```typescript
const stop = channel.streamReadReceipts((receipts) => {
  // receipts: { [timetoken: string]: string[] } — timetoken to user IDs
  Object.entries(receipts).forEach(([timetoken, userIds]) => {
    console.log(`Timetoken ${timetoken} read by`, userIds);
  });
});
```

###### 1.0.0

```typescript
// Real-time updates — one event per user receipt
const stop = channel.onReadReceiptReceived((receipt: ReadReceipt) => {
  // lastReadTimetoken is a string in the JS SDK
  console.log(`User ${receipt.userId} read up to ${receipt.lastReadTimetoken}`);
});

// On-demand snapshot of all current read positions
const response = await channel.fetchReadReceipts();
response.receipts.forEach((receipt: ReadReceipt) => {
  console.log(`${receipt.userId}: ${receipt.lastReadTimetoken}`);
});
```

### Controlling which channel types emit read receipts

A new `emitReadReceiptEvents` option in `ChatConfig` controls which channel types produce read receipt signals. By default, `public` channels do not emit read receipts.

```typescript
const chat = await Chat.init({
  publishKey: "...",
  subscribeKey: "...",
  userId: "...",
  // Default: public = false, group = true, direct = true
  // To restore the old behavior (always emit), set all to true:
  emitReadReceiptEvents: {
    public: true,
    group: true,
    direct: true,
  },
});
```

:::warning Behavioral change
In `1.0.0`, `setLastReadMessage()`, `setLastReadMessageTimetoken()`, and `markAllMessagesAsRead()` now emit `receipt` events by default on `direct` and `group` channels. If your app previously relied on these methods not emitting events, you may see unexpected receipt signals and additional network traffic after upgrading. Set `emitReadReceiptEvents` to `{ direct: false, group: false }` to restore the old behavior.
:::

## Message reactions

:::warning Important change
This change affects any code that reads `Message.reactions`. Update it before releasing to users.
:::

In `0.x.x`, `Message.reactions` returned a raw object of reaction lists. Grouping reactions by value, counting them, or checking whether the current user had reacted required manual processing.

In `1.0.0`, `Message.reactions` returns `MessageReaction[]`. Each `MessageReaction` aggregates reactions of the same value and exposes `isMine` (whether the current user is among them) and `userIds` (array of reacting user IDs). The `Message.actions` property is deprecated.

###### 0.x.x

```typescript
// Raw object — manual grouping required
const rawReactions = message.reactions;
Object.entries(rawReactions).forEach(([type, actions]) => {
  console.log(`${type}: ${actions.length} reaction(s)`);
  const isMine = actions.some((a) => a.uuid === chat.currentUser.id);
  console.log("Mine:", isMine);
});
```

###### 1.0.0

```typescript
// MessageReaction[] — aggregated by reaction value
const reactions: MessageReaction[] = message.reactions;
reactions.forEach((reaction) => {
  console.log(`${reaction.value}: ${reaction.userIds.length} reaction(s), mine: ${reaction.isMine}`);
  console.log("Reacted by:", reaction.userIds);
});
```

## Sending messages in 1.0.0

`1.0.0` separates message sending into two distinct paths: a lightweight API for plain text and a composition API for rich content.

| Use case | API |
| --- | --- |
| Send plain text with delivery options (`meta`, `ttl`, `storeInHistory`, and so on) | `channel.sendText(text, options?: SendTextParams)` |
| Build messages with mentions, channel references, links, files, or quoted messages | `channel.createMessageDraft()` → `draft.send()` |
| Migrate existing code that passed `quotedMessage` or `files` to the old `sendText()` | `channel.sendTextLegacy()` — migration only, do not use for new code |

`SendTextParams` covers lightweight delivery options only: `meta`, `storeInHistory`, `sendByPost`, `ttl`, and `customPushData`. It does not accept rich-content parameters.

For rich composition, `channel.createMessageDraft()` returns the `MessageDraft` class (previously `MessageDraftV2` in `0.x.x`). Use it whenever your UI has a text composer that handles mentions, channel references, links, files, or quoted messages.

:::warning Deprecated overload
The previous `sendText()` overload accepting `SendTextOptionParams` (which included `quotedMessage` and `files` as top-level options) was renamed to `sendTextLegacy()`.
`sendTextLegacy()` is kept only to ease migration from `0.x.x`. Do not use it for new integrations — use `sendText()` for plain text or `createMessageDraft()` for rich content.
:::

### Simple send

```typescript
const channel = await chat.getChannel("support")
await channel.sendText("Hi everyone!", {
  meta: { priority: "high" },
  storeInHistory: true,
  ttl: 15,
})
```

### Rich send with MessageDraft

```typescript
const channel = await chat.getChannel("support")
if (!channel) throw new Error("Channel not found")

const draft = channel.createMessageDraft({
  userSuggestionSource: "channel",
  isTypingIndicatorTriggered: true,
  userLimit: 10,
  channelLimit: 10,
})

draft.addChangeListener(async (state) => {
  const elements = state.messageElements
  const suggestions = await state.suggestedMentions
  renderComposer(elements)
  renderSuggestions(suggestions)
})

draft.update("Hey Alex, please check #offtopic and this link")
draft.addMention(4, 4, "mention", "alex_d")
draft.addMention(23, 9, "channelReference", "offtopic")
draft.files = [selectedFile]

await draft.send({
  meta: { source: "composer" },
  storeInHistory: true,
  ttl: 24,
})
```

### Migrating from 0.x.x

#### 0.x.x

```typescript
await channel.sendText("See you there!", {
  quotedMessage: someMessage,
  mentionedUsers: { 8: mentionedUser },
  files: [myFile],
});
```

#### 1.0.0

```typescript
// Plain text with delivery options
const params: SendTextParams = {
  meta: { priority: "high" },
  storeInHistory: true,
  ttl: 24,
};
await channel.sendText("See you there!", params);

// Rich content — use MessageDraft
// sendTextLegacy() is available only for migration from 0.x.x and will be removed in a future release
```

## removeThread() return type

In `0.x.x`, `removeThread()` returned a composite object. In `1.0.0`, it returns `Promise<boolean>`.

### 0.x.x

```typescript
const { threadChannel, parentMessage } = await message.removeThread()
console.log("Thread removed from:", threadChannel.id)
```

### 1.0.0

```typescript
const success = await message.removeThread()
console.log("Thread removed:", success)
```

## createThread() now requires text

In `0.x.x`, `createThread()` took no arguments and created an empty thread channel. In `1.0.0`, `createThread(text, options?)` requires the first reply text, sends it as the initial thread message, and returns `CreateThreadResult` containing both `threadChannel` and `parentMessage`.

### 0.x.x

```typescript
const threadChannel = await message.createThread()
await threadChannel.sendText("First reply")
```

### 1.0.0

```typescript
const { threadChannel, parentMessage } = await message.createThread("First reply")
console.log("Thread created:", threadChannel.id)
console.log("Parent has thread:", parentMessage.hasThread) // true
```

## Soft-delete removed from entity deletion

In `0.x.x`, `Channel.delete()`, `ThreadChannel.delete()`, `User.delete()`, `Chat.deleteChannel()`, and `Chat.deleteUser()` accepted a `soft` parameter for soft-deletion. In `1.0.0`, the `soft` parameter is removed — all deletions are permanent. Return types are simplified to `Promise<true>`.

### 0.x.x

```typescript
await channel.delete({ soft: true })
```

### 1.0.0

```typescript
const deleted = await channel.delete()
console.log("Channel deleted:", deleted)
```

:::note
Message soft-delete (`Message.delete({ soft: true })`) is unaffected by this change.
:::

## Membership.update() signature expanded

In `0.x.x`, `Membership.update()` accepted only `custom`. In `1.0.0`, it also accepts `status` and `type`.

### 0.x.x

```typescript
await membership.update({ custom: { role: "admin" } })
```

### 1.0.0

```typescript
await membership.update({
  status: "active",
  type: "admin",
  custom: { role: "admin" },
})
```

## MessageDraft class rename

What was `MessageDraftV2` is now `MessageDraft`. What was `MessageDraft` is now `MessageDraftV1` (deprecated). The `channel.createMessageDraft()` method now returns a `MessageDraft` (the new class). To access the old draft class during a transition period, use `channel.createMessageDraftV1()`.

### 0.x.x

```typescript
// Old primary draft
const draft = channel.createMessageDraft();
// draft is MessageDraft (old class)

// New draft
const draftV2 = channel.createMessageDraftV2();
// draftV2 is MessageDraftV2
```

### 1.0.0

```typescript
// createMessageDraft() now returns the new MessageDraft class
const draft = channel.createMessageDraft();
// draft is MessageDraft (previously MessageDraftV2)

// Old draft is now deprecated
const oldDraft = channel.createMessageDraftV1();
// oldDraft is MessageDraftV1 (previously MessageDraft), deprecated
```

### Additional MessageDraft changes

Beyond the class rename, several `MessageDraft` types changed in `1.0.0`:

* **MessageDraft.send()** — The options parameter type changed from `MessageDraftOptions` to `SendTextParams`.
* **MessageDraft.addMention()** — The mention type parameter changed from `TextTypes` to `MentionType`.
* **SuggestedMention.type** — Changed from `TextTypes` to `MentionType`.

Update any code that references these types directly.

## Custom events

`chat.emitEvent()` and `chat.listenForEvents()` are deprecated. Use `channel.emitCustomEvent()` to emit a custom event and `channel.onCustomEvent()` to listen for one.

Note that the JavaScript `emitCustomEvent()` and `onCustomEvent()` methods accept an options object rather than positional parameters.

### 0.x.x

```typescript
// Emit
await chat.emitEvent({
  channel: "my-channel",
  type: "custom",
  payload: { action: "ping" },
});

// Listen
const stop = chat.listenForEvents({
  channel: "my-channel",
  type: "custom",
  callback: (event) => {
    console.log("Custom event:", event.payload);
  },
});
```

### 1.0.0

```typescript
// Emit — options object
await channel.emitCustomEvent({ action: "ping" }, {
  storeInHistory: true,
});

// Listen — callback first, options second
const stop = channel.onCustomEvent(
  (event) => {
    console.log("Custom event:", event.payload);
  },
  { /* CustomEventListenOptions */ }
);
```

## GetCurrentUserMentionsResult

:::warning Important change
This change affects any code that reads `GetCurrentUserMentionsResult`. Update it before releasing to users.
:::

The `enhancedMentionsData` field on `GetCurrentUserMentionsResult` is renamed to `mentions`. Its element type changes to a unified `UserMention` type.

###### 0.x.x

```typescript
const result = await chat.getCurrentUserMentions();
result.enhancedMentionsData.forEach((mentionData) => {
  // mentionData could be UserMentionData, ChannelMentionData, or ThreadMentionData
  console.log("Channel:", mentionData.channelId);
});
```

###### 1.0.0

```typescript
const result = await chat.getCurrentUserMentions();
result.mentions.forEach((userMention: UserMention) => {
  console.log("Channel:", userMention.channelId);
  console.log("By:", userMention.userId);
  console.log("Message:", userMention.message.timetoken);
  if (userMention.parentChannelId) {
    console.log("Thread in:", userMention.parentChannelId);
  }
});
```

## New capabilities in 1.0.0

The following features are new in `1.0.0` and have no counterpart in `0.x.x`.

| Feature | Description |
| --- | --- |
| **channel.fetchReadReceipts()** | Returns a `ReadReceiptsResponse` with the last-read timetoken (`string`) for each channel member. Use for initial load or on-demand checks. |
| **channel.hasMember(userId)** | Returns `Promise<boolean>` — checks whether a user is a member of the channel without fetching the full membership list. |
| **channel.getMember(userId)** | Returns `Promise<Membership | null>` — retrieves a specific member's membership object directly from the channel. |
| **user.isMemberOf(channelId)** | Returns `Promise<boolean>` — checks whether the user is a member of the given channel. |
| **user.getMembership(channelId)** | Returns `Promise<Membership | null>` — retrieves the user's membership for a specific channel. |
| **membership.onUpdated()** | Fires when membership metadata changes. Returns `() => void`. |
| **membership.onDeleted()** | Fires when the membership is deleted. Returns `() => void`. |
| **channel.emitCustomEvent()** | Emits a custom event directly on the channel. Accepts `(payload, options?: CustomEventEmitOptions)`. Replaces `chat.emitEvent()`. |
| **channel.onCustomEvent()** | Subscribes to custom events on the channel. Accepts `(callback, options?: CustomEventListenOptions)`. Replaces `chat.listenForEvents({ type: "custom" })`. |
| **user.onMentioned()** | Subscribes to mention events for the user. Delivers typed `Mention` objects. |
| **user.onInvited()** | Subscribes to invite events for the user. Delivers typed `Invite` objects. |
| **user.onRestrictionChanged()** | Subscribes to moderation events for the user. Delivers restriction objects with shape `{ userId, channelId, ban, mute, reason? }`. |
| **emitReadReceiptEvents config** | `ChatConfig.emitReadReceiptEvents` (`{ public?: boolean; group?: boolean; direct?: boolean }`) controls which channel types emit read receipt signals. |
| **ChannelGroup.onMessageReceived()** | Subscribes to incoming messages on all channels in the group. |
| **ChannelGroup.onPresenceChanged()** | Subscribes to presence events across all channels in the group. |
| **membership.delete()** | Removes the membership. Returns `Promise<boolean>`. |
| **channel.getInvitees()** | Returns a list of users invited to the channel. |
| **threadChannel.onThreadMessageReceived()** | Subscribes to new messages in a thread. Returns `() => void`. |
| **threadChannel.onThreadChannelUpdated()** | Fires when thread channel metadata changes. Returns `() => void`. |
| **threadMessage.onThreadMessageUpdated()** | Fires when a thread message or its reactions change. Returns `() => void`. |

## Custom interface implementations

If your application implements or extends Chat SDK interfaces (such as `Channel`, `User`, `Message`, or `Membership`), `1.0.0` adds new required methods to these interfaces. Compilation will fail until you implement the new methods.

Key additions include entity-owned callback methods (`onUpdated()`, `onDeleted()`, `onMessageReceived()`, etc.) and new membership/presence methods (`hasMember()`, `getMember()`, `isMemberOf()`, `getMembership()`).

Review the entity documentation for each interface your code extends to identify the new required members:

* [Channel](https://www.pubnub.com/docs/chat/chat-sdk/learn/chat-entities/channel)
* [User](https://www.pubnub.com/docs/chat/chat-sdk/learn/chat-entities/user)
* [Message](https://www.pubnub.com/docs/chat/chat-sdk/learn/chat-entities/message)
* [Membership](https://www.pubnub.com/docs/chat/chat-sdk/learn/chat-entities/membership)

## Migration steps

To migrate from JavaScript Chat SDK `0.x.x` to `1.0.0`:

1. Replace every `channel.join(callback, custom?)` call. Remove the callback argument from `join()`. Add `channel.onMessageReceived(callback)` after a successful join to restore message delivery. Update your code to use `Membership` directly from the `join()` result — `JoinResult` no longer exists.
2. Rename streaming methods on `Channel`: `connect()` → `onMessageReceived()`, `getTyping()` → `onTypingChanged()`, `streamPresence()` → `onPresenceChanged()`. Update all disconnect calls: previously `joinResult.disconnect()`, now the return value of `onMessageReceived()`.
3. Replace `channel.streamUpdates()` with `channel.onUpdated()`. Add `channel.onDeleted()` wherever you previously detected deletion through the `streamUpdates()` callback. Note: `streamUpdatesOn()` is still supported and does not need to be replaced.
4. Apply the same `onUpdated()` / `onDeleted()` pattern to `User`, `Membership`, and `Message`.
5. Replace `channel.streamReadReceipts()` with `channel.onReadReceiptReceived()`. Update the handler to use `ReadReceipt` (fields: `userId`, `lastReadTimetoken: string`) instead of the old dictionary type.
6. Replace `channel.streamMessageReports()` with `channel.onMessageReported()` and update the handler to use the typed `MessageReport` type.
7. Replace `chat.listenForEvents({ type: "mention" })` with `user.onMentioned()` and update the handler to use the `Mention` type.
8. Replace `chat.listenForEvents({ type: "invite" })` with `user.onInvited()` and update the handler to use the `Invite` type.
9. Replace `chat.listenForEvents({ type: "moderation" })` with `user.onRestrictionChanged()` and update the handler to use the restriction object shape `{ userId, channelId, ban, mute, reason? }`.
10. Update all code that reads `Message.reactions`. Replace raw object iteration with `MessageReaction[]` iteration. Access `reaction.value`, `reaction.isMine`, and `reaction.userIds` directly. Remove any references to `Message.actions`.
11. Migrate any calls to the old `sendText()` multi-parameter overload with `quotedMessage`, `mentionedUsers`, and `files` to `channel.createMessageDraft()` for new integrations. For plain text sends, use the new `sendText(text, options?: SendTextParams)` form. `sendTextLegacy()` exists only to ease short-term migration from `0.x.x` and will be removed in a future release — do not use it as the primary path for new code.
12. Replace `channel.createMessageDraftV2()` calls with `channel.createMessageDraft()`. Rename any `MessageDraftV2` type references to `MessageDraft`. If you need the old draft during a transitional period, use `channel.createMessageDraftV1()` and the `MessageDraftV1` type.
13. Replace `chat.emitEvent()` calls with `channel.emitCustomEvent(payload, options?)`. Replace `chat.listenForEvents({ type: "custom" })` calls with `channel.onCustomEvent(callback, options?)`. Remove all references to `EventContent` types.
14. Replace `result.enhancedMentionsData` with `result.mentions` in any code that calls `chat.getCurrentUserMentions()`. Update element handling to use `UserMention` (fields: `message`, `userId`, `channelId`, `parentChannelId`). Note that `message` is a `Message` object, not a timetoken. `enhancedMentionsData` is deprecated but still available.
15. Review `Chat.init()` configuration and add `emitReadReceiptEvents` if you need non-default read receipt emission behavior.
16. Remove the `soft` parameter from all `Channel.delete()`, `User.delete()`, `Chat.deleteChannel()`, and `Chat.deleteUser()` calls. Update return-type handling to `Promise<true>`.
17. Update `Membership.update()` calls to use the expanded signature (`status`, `type`, `custom`).
18. Update any code that destructures `Message.removeThread()` results — the method now returns `Promise<boolean>`.
19. Replace `message.createThread()` (no arguments) with `message.createThread(text, options?)`. Handle the `CreateThreadResult` return value containing `threadChannel` and `parentMessage`.
20. Update `MessageDraft.send()` options to use `SendTextParams`. Replace `TextTypes` with `MentionType` in `addMention()` and `SuggestedMention.type`.
21. If you implement Chat SDK interfaces, add the new required methods to your implementations.
22. If you use `streamUpdates()` callbacks, be aware that they may now receive `null` for deletion events. Splitting into separate `onUpdated()` and `onDeleted()` handlers avoids this ambiguity.

If you encounter issues during migration, [contact PubNub Support](https://support.pubnub.com).