---
source_url: https://www.pubnub.com/docs/chat/unity-chat-sdk/migrate/unity-chat-v20-migration-guide
title: Unity Chat SDK 2.0.0 migration guide
updated_at: 2026-05-20T11:04:55.718Z
---

> 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


# Unity Chat SDK 2.0.0 migration guide

The 2.0.0 release of the Unity Chat SDK introduces a set of API improvements that make the SDK more consistent, more expressive, and better suited for large-scale chat applications. The changes include a revised channel join/connect lifecycle, entity-specific event names that are easier to discover, structured types for event payloads that previously required manual parsing, and a reworked read-receipt and reaction model.

This guide is for developers who use Unity Chat SDK `1.x.x` in existing applications.

If your application uses `1.x.x`, it will not compile against `2.0.0` without changes. This guide summarizes the differences between the versions and shows how to migrate to Unity Chat SDK `2.0.0`.

Most notable changes include:

* **JoinChannel and Connect replace Join**: Joining a channel no longer starts message delivery automatically. Call `JoinChannel()` to create the membership and `Connect()` separately to start receiving messages. A matching `LeaveChannel()` and `Disconnect()` replace `Leave()`.
* **Renamed events on entities**: Events on `Channel`, `User`, `Membership`, and `Message` objects were renamed to shorter, entity-agnostic names. For example, `OnChannelUpdate` is now `OnUpdated` and `OnUserUpdated` is now `OnUpdated`. Deletion events are now separate (`OnDeleted`), and the `ChatEntityChangeType` parameter was removed from all `StreamUpdatesOn` callbacks.
* **Typed event payloads**: Events that previously delivered raw `ChatEvent` objects now deliver purpose-built types. `OnMessageReported` delivers `MessageReport`, `OnMentioned` delivers `Mention`, `OnInvited` delivers `Invite`, and `OnRestrictionChanged` delivers `ChannelRestriction`.
* **New read receipt format**: `OnReadReceiptEvent` now fires once per individual user receipt rather than aggregating all memberships into a dictionary. A new `ReadReceipt` struct carries `UserId` and `LastReadTimeToken`. A `GetReadReceipts()` method provides the full snapshot when needed.
* **New message reaction format**: A new `MessageReactions()` method returns reactions grouped by value with a `Count` and an `IsMine` flag, replacing the need to iterate raw `MessageAction` lists.
* **SendTextParams simplified**: `MentionedUsers`, `QuotedMessage`, and `Files` were removed from `SendTextParams` and must now be set through `MessageDraft`.
* **Soft deletion deprecated**: The `bool soft` parameter on delete methods for Users and Channels is deprecated. Hard delete is now the default behavior.

## Differences between 1.x.x and 2.0.0

| Feature/Method | `1.x.x` | `2.0.0` |
| --- | --- | --- |
| Joining a channel | `Join()` implicitly calls `Connect()` | `JoinChannel()` and `Connect()` are separate calls |
| Leaving a channel | `Leave()` | `LeaveChannel()` and `Disconnect()` |
| Channel update event | `OnChannelUpdate` | `OnUpdated` |
| Channel/user/membership deletion event | Part of the `OnUpdate` callback with `ChatEntityChangeType` | Separate `OnDeleted` event |
| Message update event | `OnMessageUpdated` | `OnUpdated` |
| User update event | `OnUserUpdated` | `OnUpdated` |
| Membership update event | `OnMembershipUpdated` | `OnUpdated` |
| `StreamUpdatesOn` callback signature | `Action<T, ChatEntityChangeType>` | `Action<T>` (change type removed) |
| Report event on `Channel` | `OnReportEvent` with raw `ChatEvent` payload | `OnMessageReported` with typed `MessageReport` |
| Mention event on `User` | `OnMentionEvent` with raw `ChatEvent` payload | `OnMentioned` with typed `Mention` |
| Invite event on `User` | `OnInviteEvent` with raw `ChatEvent` payload | `OnInvited` with typed `Invite` |
| Moderation event on `User` | `OnModerationEvent` with raw `ChatEvent` payload | `OnRestrictionChanged` with typed `ChannelRestriction` |
| Read receipt event payload | `Dictionary<string, List<string>>` (timetoken → user IDs) | `ReadReceipt { UserId, LastReadTimeToken }` |
| Read receipt snapshot | No dedicated method | `Channel.GetReadReceipts()` |
| Message reactions | `message.Reactions` → raw `List<MessageAction>` | `message.MessageReactions()` → `List<MessageReaction>` with `Count` and `IsMine` |
| Mentioning, quoting, and files in `SendText` | Set on `SendTextParams` | Set on `MessageDraft` before calling `draft.Send()` |
| Custom events | `chat.EmitEvent(...)` (public) | `chat.EmitEvent(...)` is now internal; use `channel.EmitCustomEvent(...)` |
| Emitting user mentions | `channel.EmitUserMention(...)` (public) | `channel.EmitUserMention(...)` is now protected |
| Soft delete | `Delete(bool soft = false)` | `bool soft` parameter deprecated for Users and Channels; delete is hard by default |
| Read receipt emission control | Always emitted | Controlled per channel type via `PubnubChatConfig.EmitReadReceiptEvents` |
| `CreateDirectConversation` / `CreateGroupConversation` membership data | Single `membershipData` parameter | `hostMembershipData` + `inviteeMembershipData` parameters |
| Membership deletion | Not available from `Membership` object | `membership.Delete()` |

## JoinChannel and Connect

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

In `1.x.x`, calling `Join()` on a channel also subscribed to that channel automatically — messages started arriving immediately. In `2.0.0`, `JoinChannel()` creates the membership server-side but does **not** start message delivery. Call `Connect()` separately after `JoinChannel()` to begin receiving messages.

The reverse operation has also changed: use `LeaveChannel()` and `Disconnect()` instead of `Leave()`.

###### 1.x.x

```csharp
// Join implicitly started message delivery
await channel.Join();

// Messages arrived via channel.OnMessageReceived
```

###### 2.0.0

```csharp
// JoinChannel creates the server-side membership
var joinResult = await channel.JoinChannel();
if (joinResult.Error) { return; }

// Connect starts message delivery
channel.Connect();

// Leave:
channel.Disconnect();
await channel.LeaveChannel();
```

## Renamed events on Channel

Several `Channel` events have been renamed and split for clarity.

`OnChannelUpdate` is renamed to `OnUpdated`. The previously overloaded `OnUpdate` callback (which received a `ChatEntityChangeType` parameter) is removed — hard deletion is now signalled by the new `OnDeleted` event.

`OnReportEvent` is renamed to `OnMessageReported`. The payload changes from a raw `ChatEvent` to a structured `MessageReport`.

`OnReadReceiptEvent` remains, but its payload type changes from `Dictionary<string, List<string>>` to the new `ReadReceipt` struct. See [Read receipts](#read-receipts) for full details.

### 1.x.x

```csharp
channel.OnChannelUpdate += (updatedChannel) =>
{
    Debug.Log($"Channel updated: {updatedChannel.Id}");
};

channel.OnUpdate += (updatedChannel, changeType) =>
{
    if (changeType == ChatEntityChangeType.Delete)
    {
        Debug.Log("Channel was deleted");
    }
};

channel.OnReportEvent += (chatEvent) =>
{
    Debug.Log($"Message reported: {chatEvent.Payload}");
};
```

### 2.0.0

```csharp
channel.OnUpdated += (updatedChannel) =>
{
    Debug.Log($"Channel updated: {updatedChannel.Id}");
};

channel.OnDeleted += () =>
{
    Debug.Log("Channel was deleted");
};

channel.OnMessageReported += (report) =>
{
    Debug.Log($"Message reported — reason: {report.Reason}, by user: {report.ReportedUserId}");
};
```

## Renamed events on User

`OnUserUpdated` is renamed to `OnUpdated`. Deletion is now signalled by the separate `OnDeleted` event rather than through the overloaded `OnUpdate` callback.

Events that previously delivered raw `ChatEvent` objects now carry purpose-built types:

| Old event | New event | New payload type |
| --- | --- | --- |
| `OnMentionEvent` | `OnMentioned` | `Mention` |
| `OnInviteEvent` | `OnInvited` | `Invite` |
| `OnModerationEvent` | `OnRestrictionChanged` | `ChannelRestriction` |

### 1.x.x

```csharp
user.OnUserUpdated += (updatedUser) => { Debug.Log(updatedUser.Id); };

user.OnMentionEvent += (chatEvent) =>
{
    Debug.Log($"Mentioned in: {chatEvent.ChannelId}");
};

user.OnInviteEvent += (chatEvent) =>
{
    Debug.Log($"Invited to: {chatEvent.ChannelId}");
};

user.OnModerationEvent += (chatEvent) =>
{
    Debug.Log($"Moderation event: {chatEvent.Payload}");
};
```

### 2.0.0

```csharp
user.OnUpdated += (updatedUser) => { Debug.Log(updatedUser.Id); };

user.OnDeleted += () => { Debug.Log("User was deleted"); };

user.OnMentioned += (mention) =>
{
    Debug.Log($"Mentioned in: {mention.ChannelId}, text: {mention.Text}");
};

user.OnInvited += (invite) =>
{
    Debug.Log($"Invited to: {invite.ChannelId} by: {invite.InvitedByUserId}");
};

user.OnRestrictionChanged += (restriction) =>
{
    Debug.Log($"Channel: {restriction.ChannelId}, ban: {restriction.Ban}, mute: {restriction.Mute}");
};
```

## Renamed events on Membership and Message

`OnMembershipUpdated` on `Membership` is renamed to `OnUpdated`. `OnMessageUpdated` on `Message` is renamed to `OnUpdated`. Both entities gain a separate `OnDeleted` event for hard deletion.

```csharp
// Before
membership.OnMembershipUpdated += (m) => { };
message.OnMessageUpdated += (m) => { };

// After
membership.OnUpdated += (m) => { };
membership.OnDeleted += () => { };

message.OnUpdated += (m) => { };
```

## StreamUpdatesOn callback signatures

The `ChatEntityChangeType` parameter has been removed from all `StreamUpdatesOn` callback signatures. Update callbacks on `Channel`, `User`, `Membership`, and `Message`.

### 1.x.x

```csharp
Channel.StreamUpdatesOn(channels, (channel, changeType) =>
{
    Debug.Log($"Channel changed: {changeType}");
});

Membership.StreamUpdatesOn(memberships, (membership, changeType) =>
{
    Debug.Log($"Membership changed: {changeType}");
});
```

### 2.0.0

```csharp
Channel.StreamUpdatesOn(channels, (channel) =>
{
    Debug.Log($"Channel updated: {channel.Id}");
});

Membership.StreamUpdatesOn(memberships, (membership) =>
{
    Debug.Log($"Membership updated for: {membership.Channel.Id}");
});
```

The same change applies to `User.StreamUpdatesOn` and `Message.StreamUpdatesOn`.

## Read receipts

:::warning Important change
This change affects any code that reads `OnReadReceiptEvent` payload. Update the handler before releasing to users.
:::

In `1.x.x`, `OnReadReceiptEvent` fired with a `Dictionary<string, List<string>>` where keys were timetokens and values were lists of user IDs who had read up to that point. Computing this required a call to `GetChannelMemberships` on every receipt event.

In `2.0.0`:

* `OnReadReceiptEvent` fires once per incoming receipt signal, delivering a single `ReadReceipt` value that contains the user ID and their last-read timetoken. No membership fetch is required.
* `Channel.GetReadReceipts()` provides an on-demand snapshot of all current read receipts as `List<ReadReceipt>`.

###### 1.x.x

```csharp
channel.OnReadReceiptEvent += (Dictionary<string, List<string>> receipts) =>
{
    foreach (var kvp in receipts)
    {
        string timetoken = kvp.Key;
        List<string> userIds = kvp.Value;
        Debug.Log($"Timetoken {timetoken} read by {userIds.Count} users");
    }
};
```

###### 2.0.0

```csharp
// Real-time updates — one event per user receipt
channel.OnReadReceiptEvent += (ReadReceipt receipt) =>
{
    Debug.Log($"User {receipt.UserId} read up to {receipt.LastReadTimeToken}");
};

// On-demand snapshot of all current receipts
var result = await channel.GetReadReceipts();
foreach (ReadReceipt receipt in result.Result)
{
    Debug.Log($"{receipt.UserId}: {receipt.LastReadTimeToken}");
}
```

### Controlling which channel types emit read receipts

A new `EmitReadReceiptEvents` property in `PubnubChatConfig` controls which channel types produce read receipt signals. The default setting disables read receipts on `public` channels, which is appropriate for most large-scale deployments.

```csharp
// Default: public = false, group = true, direct = true
// To restore old behavior (always emit), set all to true:
var config = new PubnubChatConfig(publishKey, subscribeKey, userId,
    emitReadReceiptEvents: new Dictionary<string, bool>
    {
        { "public", true },
        { "group",  true },
        { "direct", true }
    });
```

## Message reactions

In `1.x.x`, the raw `message.Reactions` property returned a flat `List<MessageAction>`. Grouping reactions by type, counting them, or checking whether the current user had reacted required manual processing.

In `2.0.0`, the new `message.MessageReactions()` method returns `List<MessageReaction>`. Each `MessageReaction` aggregates reactions of the same value and exposes `Count` (how many users reacted) and `IsMine` (whether the current user is among them). The raw `message.Reactions` property remains available for backwards-compatible reading.

### 1.x.x

```csharp
// Raw list — manual grouping required
var reactions = message.Reactions;
var grouped = reactions
    .GroupBy(r => r.Value)
    .Select(g => new { Value = g.Key, Count = g.Count() });

foreach (var group in grouped)
{
    Debug.Log($"{group.Value}: {group.Count}");
}
```

### 2.0.0

```csharp
// Aggregated by reaction value — count and IsMine included
var reactions = message.MessageReactions();
foreach (MessageReaction reaction in reactions)
{
    Debug.Log($"{reaction.Value}: {reaction.Count} reaction(s), mine: {reaction.IsMine}");
}
```

## SendTextParams and MessageDraft

`MentionedUsers`, `QuotedMessage`, and `Files` have been removed from `SendTextParams` and are now properties on `MessageDraft`. To send a message with mentions, a quoted message, or attachments, create a draft, configure it, and send.

### 1.x.x

```csharp
await channel.SendText("See you there!", new SendTextParams
{
    QuotedMessage = someMessage,
    MentionedUsers = new Dictionary<int, User> { { 8, mentionedUser } },
    Files = new List<InputFile> { myFile }
});
```

### 2.0.0

```csharp
var draft = channel.CreateMessageDraft();
draft.InsertText(0, "See you there!");
draft.QuotedMessage = someMessage;
draft.AddMention(8, mentionedUser.Id.Length, MentionTarget.User(mentionedUser.Id));
draft.Files = new List<InputFile> { myFile };
await draft.Send();
```

A new `AppendText()` convenience method is also available:

```csharp
draft.AppendText(" — see you there!");
```

## Custom events

`chat.EmitEvent()` is now `internal` and can no longer be called from application code. Use `channel.EmitCustomEvent()` instead, passing the channel you want to emit to directly.

### 1.x.x

```csharp
await chat.EmitEvent(PubnubChatEventType.Custom, "my-channel", "{\"key\":\"value\"}");
```

### 2.0.0

```csharp
await channel.EmitCustomEvent("{\"key\":\"value\"}", storeInHistory: true);
```

## CreateDirectConversation and CreateGroupConversation

Both conversation-creation methods now accept separate membership data for the host and for the invited user(s).

### 1.x.x

```csharp
await chat.CreateDirectConversation(user, channelId, channelData,
    membershipData: hostMembershipData);

await chat.CreateGroupConversation(users, channelId, channelData,
    membershipData: hostMembershipData);
```

### 2.0.0

```csharp
await chat.CreateDirectConversation(user, channelId, channelData,
    hostMembershipData: hostData,
    inviteeMembershipData: inviteeData);

await chat.CreateGroupConversation(users, channelId, channelData,
    hostMembershipData: hostData,
    inviteesMembershipData: inviteesData);
```

## Soft deletion deprecated

The `bool soft` parameter on delete methods is deprecated. If you rely on soft deletion (marking entities as deleted while retaining data), migrate to explicit hard deletion using the new parameter-free `Delete()` methods. If you need to preserve the soft-delete behavior during a transitional period, the old overloads still compile, but they will be removed in a future version.

```csharp
// Deprecated — soft parameter will be removed
await user.DeleteUser(soft: true);
await channel.Delete(soft: false);

// Preferred in 2.0.0
await user.Delete();
await channel.Delete();
await membership.Delete(); // new: Delete() is now available directly on Membership
```

## New capabilities in 2.0.0

The following features are new in `2.0.0` and have no counterpart in `1.x.x`.

| Feature | Description |
| --- | --- |
| **GetReadReceipts()** | `await channel.GetReadReceipts()` returns a paginated `List<ReadReceipt>` snapshot of the current read state for all channel members. |
| **MessageReactions()** | `message.MessageReactions()` returns reactions grouped by value with `Count` and `IsMine` fields. |
| **HasMember() / GetMember()** | `await channel.HasMember(userId)` and `await channel.GetMember(userId)` let you check and retrieve a specific member without fetching the full membership list. |
| **IsMemberOn() / GetMembership()** | `await user.IsMemberOn(channelId)` and `await user.GetMembership(channelId)` let you check and retrieve a specific membership from the `User` object. |
| **Membership.Delete()** | `await membership.Delete()` deletes a membership directly from the `Membership` object. |
| **ReconnectSubscriptions() / DisconnectSubscriptions()** | `chat.ReconnectSubscriptions()` restores all active subscriptions; `chat.DisconnectSubscriptions()` drops them cleanly. |
| **OnSubscriptionStatusChanged** | `chat.OnSubscriptionStatusChanged` fires with a `PNStatus` value whenever the underlying PubNub connection changes. Enable it with `chat.StreamSubscriptionStatus(true)`. |
| **ChatUtils (public)** | `ChatUtils.TimeTokenNow()` and `ChatUtils.TimeToken(DateTime date)` are now publicly accessible. |
| **MessageDraft.AppendText()** | Appends a plain-text string to the end of a draft without needing to compute the current cursor position. |
| **MessageDraft.GetMessageElements()** | `MessageDraft.GetMessageElements(text)` (static) and `message.GetMessageElements()` parse a message text into a structured `List<MessageElement>` for rendering mentions and links. |
| **EmitReadReceiptEvents config** | `PubnubChatConfig.EmitReadReceiptEvents` (`Dictionary<string, bool>`) controls which channel types emit read receipt signals, letting you reduce unnecessary network traffic on `public` channels. |

## Migration steps

To migrate from Unity Chat SDK `1.x.x` to `2.0.0`:

1. Replace every `channel.Join()` call with `await channel.JoinChannel()` followed by `channel.Connect()`. Replace every `channel.Leave()` call with `channel.Disconnect()` followed by `await channel.LeaveChannel()`.
2. Rename `OnChannelUpdate` to `OnUpdated` on `Channel`. Remove any `OnUpdate` handlers that checked for `ChatEntityChangeType.Delete` and replace them with `OnDeleted` handlers. Apply the same pattern to `User`, `Membership`, and `Message`.
3. Rename `OnMessageUpdated` on `Message` to `OnUpdated`.
4. Rename `OnUserUpdated` on `User` to `OnUpdated`.
5. Rename `OnMembershipUpdated` on `Membership` to `OnUpdated`.
6. Update `StreamUpdatesOn` callbacks on `Channel`, `User`, `Membership`, and `Message` to remove the `ChatEntityChangeType` parameter.
7. Rename `OnMentionEvent` to `OnMentioned` on `User` and update the handler to use the `Mention` type. Rename `OnInviteEvent` to `OnInvited` (use `Invite` type). Rename `OnModerationEvent` to `OnRestrictionChanged` (use `ChannelRestriction` type).
8. Rename `OnReportEvent` to `OnMessageReported` on `Channel` and update the handler to use the `MessageReport` type.
9. Update all `OnReadReceiptEvent` handlers to use the new `ReadReceipt` struct (`UserId`, `LastReadTimeToken`) instead of the old dictionary type. Replace any manual membership-aggregation logic with `GetReadReceipts()` where a full snapshot is needed.
10. Replace raw `message.Reactions` iteration with `message.MessageReactions()` where reaction counts or `IsMine` checks are needed.
11. Move `QuotedMessage`, `MentionedUsers`, and `Files` out of `SendTextParams` and onto a `MessageDraft` instance. Call `draft.Send()` instead of `channel.SendText()` for messages that use these features.
12. Replace `chat.EmitEvent(...)` calls with `channel.EmitCustomEvent(...)`.
13. Update `CreateDirectConversation` and `CreateGroupConversation` calls to use the renamed `hostMembershipData` and the new `inviteeMembershipData` / `inviteesMembershipData` parameters.
14. Remove the `bool soft` parameter from any `DeleteUser()`, `Channel.Delete()`, or `Chat.DeleteChannel()` calls. Migrate to the new parameter-free `Delete()` methods.
15. Review `PubnubChatConfig` initialization and add `EmitReadReceiptEvents` if you need non-default read receipt emission behavior.

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