---
source_url: https://www.pubnub.com/docs/chat/swift-chat-sdk/migrate/swift-chat-v10-migration-guide
title: Swift Chat SDK 1.0.0 migration guide
updated_at: 2026-06-30T11:05:11.369Z
---

> 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


# Swift Chat SDK 1.0.0 migration guide

The 1.0.0 release of the Swift 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 streaming methods to entity-owned callbacks: instead of calling `chat.listenForEvents()` or using `channel.connect()`, you now attach closures directly to entity objects (`channel.onMessageReceived`, `user.onMentioned`, etc.) or use the new `.stream` property for `AsyncStream`-based alternatives.

This guide is for developers who use Swift 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 `Channel.delete()` / `User.delete()`. This guide summarizes the differences between the versions and shows how to migrate to Swift Chat SDK `1.0.0`.

Most notable changes include:

* **join() signature change**: The return type changes from a tuple `(membership:, messagesStream:)` to `Membership` directly. A `messagesStream` is no longer returned — call `onMessageReceived()` or use `channel.stream.messages()` separately to start message delivery. Two new optional parameters, `status` and `type`, are added.
* **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.
* **.stream property**: Each entity exposes a `.stream` property providing `AsyncStream`-based equivalents for every callback method. For example, `channel.stream.messages()` is the `AsyncStream` equivalent of `channel.onMessageReceived`.
* **Typed event payloads**: Events that previously required manual `EventContent` parsing now deliver purpose-built types. `onMentioned()` delivers `Mention`, `onInvited()` delivers `Invite`, `onRestrictionChanged()` delivers `Restriction`, and `onReadReceiptReceived()` delivers `ReadReceipt`.
* **Message.reactions type change**: `Message.reactions` changed from `[String: [Action]]` to `[MessageReaction]`. Each `MessageReaction` exposes `value`, `count`, `isMine`, and `userIds`.
* **sendText() simplified**: A new `sendText(text:params:)` overload consolidates the previous multi-parameter signature into a `SendTextParams` struct. The old overload is deprecated.
* **createThread() consolidated**: The no-argument `createThread(completion:)` and the multi-parameter `createThread(text:meta:shouldStore:...)` overloads are deprecated. Use the new `createThread(text:params:completion:)` instead, which accepts a `SendTextParams` struct and returns a `CreateThreadResult` containing both the new `ThreadChannel` and the updated parent `Message`.
* **Soft delete removed**: The `soft:` parameter is removed from `Channel.delete()` and `User.delete()`. Both methods now always perform a hard delete.
* **New Membership.delete()**: Memberships can now be deleted directly from the `Membership` object.
* **New membership convenience methods**: `channel.hasMember()`, `channel.getMember()`, `user.isMemberOf()`, and `user.getMembership()` let you check and retrieve a specific membership without fetching the full list.
* **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 custom event methods on Channel**: `channel.emitCustomEvent()` and `channel.onCustomEvent()` replace the `chat`-level `emitEvent()` / `listenForEvents()` pattern for custom events. `chat.listenForEvents()` is deprecated.

## Differences between 0.x.x and 1.0.0

| Feature/Method | `0.x.x` | `1.0.0` |
| --- | --- | --- |
| `join()` return type | `(membership: MembershipImpl, messagesStream: AsyncStream<MessageImpl>)` | `MembershipImpl` |
| `join()` signature | `join(custom:)` | `join(custom:status:type:)` |
| Message delivery after `join()` | Via `messagesStream` in the return tuple | Requires explicit `onMessageReceived()` or `channel.stream.messages()` call |
| `connect()` | Primary method for message delivery | Deprecated — use `channel.onMessageReceived(callback:)` |
| `getTyping()` | Typing indicator `AsyncStream` | Deprecated — use `channel.onTypingChanged(callback:)` |
| `streamPresence()` | Presence `AsyncStream` | Deprecated — use `channel.onPresenceChanged(callback:)` |
| `streamUpdates()` on entity | Update `AsyncStream` (single entity) | Deprecated — use `onUpdated(callback:)` |
| Deletion events | Part of `streamUpdates()` (nil value) | Separate `onDeleted(callback:)` event |
| `streamReadReceipts()` | `AsyncStream<[Timetoken: [String]]>` (timetoken → user IDs) | Deprecated — use `channel.onReadReceiptReceived(callback:)` with typed `ReadReceipt` |
| `streamMessageReports()` | `AsyncStream<EventWrapper<EventContent.Report>>` | Deprecated — use `channel.onMessageReported(callback:)` with typed `Report` |
| Mention events | `chat.listenForEvents<EventContent.Mention>()` | `user.onMentioned(callback:)` with typed `Mention` |
| Invite events | `chat.listenForEvents<EventContent.Invite>()` | `user.onInvited(callback:)` with typed `Invite` |
| Moderation events | `chat.listenForEvents<EventContent.Moderation>()` | `user.onRestrictionChanged(callback:)` with typed `Restriction` |
| Custom events (emit) | `chat.emitEvent()` | `channel.emitCustomEvent()` |
| Custom events (listen) | `chat.listenForEvents()` | `channel.onCustomEvent(callback:)` |
| `listenForEvents()` | Available | Deprecated |
| `Message.reactions` type | `[String: [Action]]` | `[MessageReaction]` |
| `Message.actions` | Available | Deprecated — use `Message.reactions` |
| `sendText()` signature | Multi-parameter (`text:meta:shouldStore:usePost:ttl:quotedMessage:files:usersToMention:customPushData:`) | `sendText(text:params:)` with `SendTextParams`; old overload deprecated |
| `createThread()` | `createThread(completion:)` and `createThread(text:meta:shouldStore:usePost:ttl:quotedMessage:files:usersToMention:customPushData:completion:)` | `createThread(text:params:completion:)` returning `CreateThreadResult`; old overloads deprecated |
| `Channel.delete()` | `delete(soft: Bool = false)` | `delete()` — no `soft` parameter; always hard deletes |
| `User.delete()` | `delete(soft: Bool = false)` | `delete()` — no `soft` parameter; always hard deletes |
| `Membership.delete()` | Not available | `delete()` available directly on `Membership` |
| Membership existence check | `getMembers()` then filter | `channel.hasMember(userId:)`, `channel.getMember(userId:)` |
| User membership check | `getMemberships()` then filter | `user.isMemberOf(channelId:)`, `user.getMembership(channelId:)` |
| Read receipts snapshot | Not available | `channel.fetchReadReceipts()` → `ReadReceiptsResponse` |
| `emitReadReceiptEvents` config | Not available | Available in `ChatConfiguration` |
| `.stream` property on entities | Not available | `AsyncStream`-based equivalents for all callback methods |

## join() and onMessageReceived()

:::warning Important change
This change affects any code that calls `join()` and then reads from the returned `messagesStream`. Update it before releasing to users.
:::

In `0.x.x`, calling `join()` on a channel returned a tuple containing both the membership and an `AsyncStream` of messages — you could iterate the stream immediately to receive messages. In `1.0.0`, `join()` creates the membership only and returns `MembershipImpl` directly. Call `onMessageReceived(callback:)` or iterate `channel.stream.messages()` separately to start receiving messages.

Two new optional parameters, `status` and `type`, let you set membership metadata at join time.

###### 0.x.x

```swift
// join() returned a tuple with both the membership and a message stream
let result = try await channel.join(custom: ["role": "member"])
let membership = result.membership

// Messages arrived via the returned AsyncStream
Task {
    for await message in result.messagesStream {
        print("New message: \(message.text ?? "")")
    }
}
```

###### 1.0.0

```swift
// join() creates the membership and returns it directly
let membership = try await channel.join(
    custom: ["role": "member"],
    status: "active",
    type: "standard"
)

// Subscribe to messages separately — closure callback
let stopMessages = channel.onMessageReceived { message in
    print("New message: \(message.text ?? "")")
}

// Or use the AsyncStream equivalent
Task {
    for await message in channel.stream.messages() {
        print("New message: \(message.text ?? "")")
    }
}

// To stop: call close() on the returned AutoCloseable
stopMessages.close()
```

## Entity-first callbacks

In `0.x.x`, streaming subscriptions were created by calling methods like `connect()`, `getTyping()`, or `streamPresence()` that returned an `AsyncStream`. You iterated the stream in a `Task` to receive events.

In `1.0.0`, entity objects expose dedicated callback methods. Pass a closure directly to the method; it returns an `AutoCloseable` that you call `.close()` on to stop the subscription. For each callback method there is a matching `.stream.*()` method on the entity's `.stream` property that returns the `AsyncStream` equivalent.

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

### 0.x.x

```swift
// Channel — message delivery
Task {
    for await message in channel.connect() {
        print("Message: \(message.text ?? "")")
    }
}

// Channel — typing indicator
Task {
    for await typingUserIds in channel.getTyping() {
        print("Typing: \(typingUserIds)")
    }
}

// Channel — presence
Task {
    for await presence in channel.streamPresence() {
        print("Present: \(presence)")
    }
}

// Channel — metadata updates
Task {
    for await updatedChannel in channel.streamUpdates() {
        if let updated = updatedChannel {
            print("Channel updated: \(updated.id)")
        } else {
            print("Channel was deleted")
        }
    }
}

// User — metadata updates
Task {
    for await updatedUser in user.streamUpdates() {
        if let updated = updatedUser {
            print("User updated: \(updated.id)")
        }
    }
}
```

### 1.0.0

```swift
// Channel — message delivery (closure callback)
let stopMessages = channel.onMessageReceived { message in
    print("Message: \(message.text ?? "")")
}
// Or AsyncStream equivalent:
// for await message in channel.stream.messages() { }

// Channel — typing indicator
let stopTyping = channel.onTypingChanged { typingUserIds in
    print("Typing: \(typingUserIds)")
}
// Or: for await ids in channel.stream.typingChanges() { }

// Channel — presence
let stopPresence = channel.onPresenceChanged { presentUserIds in
    print("Present: \(presentUserIds)")
}
// Or: for await ids in channel.stream.presenceChanges() { }

// Channel — metadata updates (update and deletion are now separate)
let stopUpdated = channel.onUpdated { updatedChannel in
    print("Channel updated: \(updatedChannel.id)")
}
let stopDeleted = channel.onDeleted {
    print("Channel was deleted")
}
// Or AsyncStream equivalents:
// for await ch in channel.stream.updates() { }
// for await _ in channel.stream.deletions() { }

// User — metadata updates
let stopUserUpdated = user.onUpdated { updatedUser in
    print("User updated: \(updatedUser.id)")
}
let stopUserDeleted = user.onDeleted {
    print("User was deleted")
}

// All return AutoCloseable — call .close() to unsubscribe
stopMessages.close()
stopTyping.close()
```

## Typed event payloads on User

Events that previously required calling `chat.listenForEvents()` with a generic `EventContent` type and parsing the payload manually now deliver purpose-built structs directly to entity-level callbacks.

| Old method | New method | New payload type |
| --- | --- | --- |
| `chat.listenForEvents<EventContent.Mention>()` | `user.onMentioned(callback:)` | `Mention` |
| `chat.listenForEvents<EventContent.Invite>()` | `user.onInvited(callback:)` | `Invite` |
| `chat.listenForEvents<EventContent.Moderation>()` | `user.onRestrictionChanged(callback:)` | `Restriction` |

### 0.x.x

```swift
// Mention events
Task {
    for await event in chat.listenForEvents(type: EventContent.Mention.self, channelId: user.id) {
        if let payload = event.payload as? EventContent.Mention {
            print("Mentioned in channel: \(payload.channel ?? "")")
        }
    }
}

// Invite events
Task {
    for await event in chat.listenForEvents(type: EventContent.Invite.self, channelId: user.id) {
        if let payload = event.payload as? EventContent.Invite {
            print("Invited to channel: \(payload.channel ?? "")")
        }
    }
}

// Moderation events
Task {
    for await event in chat.listenForEvents(type: EventContent.Moderation.self, channelId: user.id) {
        if let payload = event.payload as? EventContent.Moderation {
            print("Moderation event in: \(payload.channel ?? "")")
        }
    }
}
```

### 1.0.0

```swift
// Mention events — typed Mention struct
let stopMentions = user.onMentioned { mention in
    print("Mentioned in channel: \(mention.channelId)")
    print("By user: \(mention.mentionedByUserId)")
}
// Or: for await mention in user.stream.mentions() { }

// Invite events — typed Invite struct
let stopInvites = user.onInvited { invite in
    print("Invited to channel: \(invite.channelId)")
    print("By user: \(invite.invitedByUserId)")
}
// Or: for await invite in user.stream.invites() { }

// Moderation events — typed Restriction struct
let stopRestrictions = user.onRestrictionChanged { restriction in
    print("Restriction in channel: \(restriction.channelId)")
    print("Banned: \(restriction.ban), Muted: \(restriction.mute)")
}
// Or: for await restriction in user.stream.restrictions() { }
```

## Read receipts

:::warning Important change
This change affects any code that iterates `streamReadReceipts()`. Update the handler before releasing to users.
:::

In `0.x.x`, `streamReadReceipts()` yielded a `[Timetoken: [String]]` dictionary where keys were timetokens and values were lists of user IDs who had read up to that point. Aggregating across users required iterating the full dictionary on every event.

In `1.0.0`:

* `onReadReceiptReceived(callback:)` fires once per incoming receipt signal, delivering a single `ReadReceipt` value containing the user ID and their last-read timetoken. No manual aggregation is required.
* `channel.fetchReadReceipts()` provides an on-demand snapshot of all current read receipts as `[ReadReceipt]`.
* A new `emitReadReceiptEvents` configuration property in `ChatConfiguration` controls which channel types emit read receipt signals.

###### 0.x.x

```swift
Task {
    for await receipts in channel.streamReadReceipts() {
        for (timetoken, userIds) in receipts {
            print("Timetoken \(timetoken) read by \(userIds.count) user(s): \(userIds)")
        }
    }
}
```

###### 1.0.0

```swift
// Real-time updates — one callback per user receipt
let stopReceipts = channel.onReadReceiptReceived { receipt in
    print("User \(receipt.userId) read up to \(receipt.lastReadTimetoken)")
}
// Or AsyncStream equivalent:
// for await receipt in channel.stream.readReceipts() { }

// On-demand snapshot of all current receipts
let response = try await channel.fetchReadReceipts()
for receipt in response.receipts {
    print("\(receipt.userId): \(receipt.lastReadTimetoken)")
}
```

### Controlling which channel types emit read receipts

A new `emitReadReceiptEvents` property in `ChatConfiguration` 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.

```swift
// Default: public = false, group = true, direct = true
// To restore old behavior (always emit), set all to true:
let config = ChatConfiguration(
    emitReadReceiptEvents: [
        .public: true,
        .group: true,
        .direct: true
    ]
)
```

## Message reactions

In `0.x.x`, `message.reactions` returned a `[String: [Action]]` dictionary. Grouping reactions by value, counting them, or checking whether the current user had reacted required manually iterating the dictionary and inspecting individual `Action` objects.

In `1.0.0`, `message.reactions` returns `[MessageReaction]`. Each `MessageReaction` aggregates reactions of the same value and exposes:

* `value` — the reaction string (for example, `"👍"`)
* `count` — derived from `userIds.count`
* `isMine` — whether the current user is among the reactors
* `userIds` — the full list of user IDs who added this reaction

The old `message.actions` property is deprecated.

### 0.x.x

```swift
// Raw dictionary — manual grouping required
for (reactionValue, actions) in message.reactions {
    print("\(reactionValue): \(actions.count) reaction(s)")
    // Checking if current user reacted required inspecting Action.uuid
}
```

### 1.0.0

```swift
// Aggregated by reaction value — count and isMine included
for reaction in message.reactions {
    print("\(reaction.value): \(reaction.count) reaction(s), mine: \(reaction.isMine)")
}
```

## sendText()

In `0.x.x`, `sendText()` accepted all message parameters as individual arguments. In `1.0.0`, a new overload consolidates the optional parameters into a `SendTextParams` struct. The old overload is deprecated.

### 0.x.x

```swift
try await channel.sendText(
    text: "Hello everyone",
    meta: ["importance": "high"],
    shouldStore: true,
    ttl: 24
)
```

### 1.0.0

```swift
let params = SendTextParams(
    meta: ["importance": "high"],
    shouldStore: true,
    ttl: 24
)
try await channel.sendText(text: "Hello everyone", params: params)
```

## createThread()

Both `createThread(completion:)` (no parameters, returning only a `ThreadChannel`) and the multi-parameter `createThread(text:meta:shouldStore:usePost:ttl:quotedMessage:files:usersToMention:customPushData:completion:)` overload are deprecated. Use `createThread(text:params:completion:)` instead. The new method accepts a `SendTextParams` struct and returns a `CreateThreadResult` containing both the new `ThreadChannel` and the updated parent `Message`.

### 0.x.x

```swift
// Create a thread and get only the ThreadChannel
message.createThread { result in
    switch result {
    case .success(let threadChannel):
        print("Thread created: \(threadChannel.id)")
    case .failure(let error):
        print("Error: \(error)")
    }
}
```

### 1.0.0

```swift
// createThread() returns both the ThreadChannel and the updated parent Message
let threadResult = try await message.createThread(
    text: "Starting this thread",
    params: SendTextParams(shouldStore: true)
)
let threadChannel = threadResult.threadChannel
let updatedMessage = threadResult.message
print("Thread channel ID: \(threadChannel.id)")
```

## Custom events

`chat.listenForEvents()` is deprecated. Use `channel.onCustomEvent()` and `channel.emitCustomEvent()` instead.

```swift
// Emit a custom event on a Channel
try await channel.emitCustomEvent(
    payload: ["chatID": "chat1234", "triggerWord": "frustrated"],
    messageType: "customer-satisfaction",
    storeInHistory: true
)

// Listen for custom events
let stopCustom = channel.onCustomEvent(messageType: "customer-satisfaction") { event in
    if let triggerWord = event.payload["triggerWord"]?.rawValue as? String {
        print("Custom event — trigger: \(triggerWord)")
    }
}
// AsyncStream: for await event in channel.stream.customEvents() { }
stopCustom.close()
```

## Soft delete removed from Channel and User

The `soft:` parameter has been removed from `Channel.delete()` and `User.delete()`. Both methods now always perform a hard (permanent) delete. If your application relied on soft deletion for any reason, migrate to hard deletion before upgrading.

A new `Membership.delete()` method is available to delete a membership directly from the `Membership` object.

### 0.x.x

```swift
// delete() accepted a soft parameter
try await channel.delete(soft: false)
try await user.delete(soft: false)

// Membership deletion required going through Chat or Channel
try await chat.deleteChannel(id: channel.id)
```

### 1.0.0

```swift
// delete() is now parameter-free — always hard deletes
try await channel.delete()
try await user.delete()

// Memberships can now be deleted directly
try await membership.delete()
```

## 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 |
| --- | --- |
| **fetchReadReceipts()** | `try await channel.fetchReadReceipts()` returns a paginated snapshot of the current read position for all channel members as `[ReadReceipt]`. |
| **onReadReceiptReceived(callback:)** | `channel.onReadReceiptReceived { receipt in }` delivers a typed `ReadReceipt` per incoming signal, replacing the raw `[Timetoken: [String]]` dictionary from `streamReadReceipts()`. |
| **onMessageReported(callback:)** | `channel.onMessageReported { report in }` delivers a typed `Report` struct, replacing `streamMessageReports()`. |
| **.stream property** | Every entity exposes a `.stream` property (for example, `channel.stream`, `user.stream`, `membership.stream`) providing `AsyncStream`-based equivalents for all callback methods. |
| **hasMember(userId:) / getMember(userId:)** | `try await channel.hasMember(userId:)` and `try await channel.getMember(userId:)` check and retrieve a specific member without fetching the full membership list. |
| **isMemberOf(channelId:) / getMembership(channelId:)** | `try await user.isMemberOf(channelId:)` and `try await user.getMembership(channelId:)` check and retrieve a specific membership from the `User` object. |
| **Membership.delete()** | `try await membership.delete()` removes a membership directly from the `Membership` object. |
| **emitCustomEvent() / onCustomEvent()** | `channel.emitCustomEvent(payload:method:)` and `channel.onCustomEvent(callback:)` provide a channel-scoped custom event API, replacing `chat.emitEvent()` / `chat.listenForEvents()`. |
| **createThread(text:params:)** | Returns a `CreateThreadResult` containing both the `ThreadChannel` and the updated parent `Message`. The earlier no-argument `createThread(completion:)` and multi-parameter `createThread(text:meta:shouldStore:...)` overloads are deprecated. |
| **getInvitees()** | `try await channel.getInvitees()` returns the list of users who have been invited to the channel. |
| **SendTextParams** | New struct consolidating the optional parameters for `sendText()` and `MessageDraft.send()`. |
| **emitReadReceiptEvents config** | `ChatConfiguration.emitReadReceiptEvents` (`[ChannelType: Bool]`) controls which channel types emit read receipt signals, letting you reduce unnecessary network traffic on `public` channels. |
| **Membership.status and Membership.type** | Two new properties on `Membership` that can be set when calling `membership.update(status:type:)`. |

## Migration steps

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

1. Update every `channel.join()` call to handle the new return type. The method now returns `MembershipImpl` directly instead of a `(membership:, messagesStream:)` tuple. Remove any code that reads `result.messagesStream` and replace it with a call to `channel.onMessageReceived(callback:)` or iteration of `channel.stream.messages()`.
2. Replace `channel.connect()` with `channel.onMessageReceived { message in }`. Store the returned `AutoCloseable` and call `.close()` to stop message delivery.
3. Replace `channel.getTyping()` with `channel.onTypingChanged { typingUserIds in }`. For `AsyncStream`, use `channel.stream.typingChanges()`.
4. Replace `channel.streamPresence()` with `channel.onPresenceChanged { presentUserIds in }`. For `AsyncStream`, use `channel.stream.presenceChanges()`.
5. Replace `channel.streamUpdates()` with separate `channel.onUpdated { }` and `channel.onDeleted { }` callbacks. The `nil` value that `streamUpdates()` emitted on deletion is replaced by the dedicated `onDeleted` event.
6. Replace `user.streamUpdates()` with `user.onUpdated { }` and `user.onDeleted { }`.
7. Replace `membership.streamUpdates()` with `membership.onUpdated { }` and `membership.onDeleted { }`.
8. Replace `channel.streamReadReceipts()` with `channel.onReadReceiptReceived { receipt in }`. The payload changes from `[Timetoken: [String]]` to the `ReadReceipt` struct (`userId`, `lastReadTimetoken`). Remove any manual aggregation logic and replace full-snapshot reads with `channel.fetchReadReceipts()`.
9. Replace `channel.streamMessageReports()` with `channel.onMessageReported { report in }`. The payload changes from `EventWrapper<EventContent.Report>` to the `Report` struct.
10. Replace `chat.listenForEvents()` for mention events with `user.onMentioned { mention in }`, for invite events with `user.onInvited { invite in }`, and for moderation events with `user.onRestrictionChanged { restriction in }`.
11. Replace any remaining `chat.listenForEvents()` usage for custom events with `channel.onCustomEvent { event in }`, and replace `chat.emitEvent()` with `channel.emitCustomEvent(payload:storeInHistory:)`. Read values from `event.payload` as `[String: JSONCodable]`.
12. Update all `message.reactions` access: the type changes from `[String: [Action]]` to `[MessageReaction]`. Replace raw `actions` property access — `message.actions` is deprecated.
13. Replace `channel.sendText(text:meta:shouldStore:ttl:...)` calls with `channel.sendText(text:params:)` using the `SendTextParams` struct. Move `meta`, `shouldStore`, `ttl`, and other options into `SendTextParams`.
14. Replace `message.createThread(completion:)` and the multi-parameter `message.createThread(text:meta:shouldStore:...)` with `message.createThread(text:params:)`. Update callsites to use the `CreateThreadResult` return type to access both `threadChannel` and the updated `message`.
15. Remove the `soft:` parameter from all `channel.delete()` and `user.delete()` calls. Both methods no longer accept this parameter and always perform a hard delete.
16. Review `ChatConfiguration` 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).