let client = ChatClient(config: config, tokenProvider: .static(token))
Migrating from 3.x to 4.x
The major version bump is due to the presence of several breaking changes. We believe they are easy to take care of in the upgrade process and improve the design of the library.
Please don’t hesitate to contact our Support team or open a ticket in our GitHub repository. We’ll help you during your migration process and any issues you might face.
What’s New in v4.x?
- UI components have an open API and are fully customizable. Review UI Customization Guide to learn more about how to customize UI components.
- Messages can have custom attachments. See Attachments v4.x Migration below.
- ExtraData is now stored in a Dictionary, instead of using generics. See ExtraData Changes below.
UIConfig
is split intoAppearance
andComponents
to improve clarity.- The user setting API was updated. See New User Setting API below.
- The inner structure of the UI components has been simplified. Many UI components were renamed.
DefaultAppearance
API was removed due to its difficulty of usage.- The majority of the UI components use
ContainerStackView
(our customUIStackView
re-implementation) for its layout.
For the list of all changes, please check out the CHANGELOG.
New User Setting API
The user setting API was completely changed in v4. Here are the comparisons:
Static Tokens
v3:
v4:
let client = ChatClient(config: config)
client.connectUser(userInfo: .init(id: userId), token: token)
Closure-based Tokens
v3:
let client = ChatClient(
config: config,
tokenProvider: .closure { client, completion in
service.fetchToken { token in
completion(token)
}
}
)
v4:
// `tokenProvider` property is used to reobtain a new token in case if the current one is expired
let client = ChatClient(config: config, tokenProvider: { completion in
service.fetchToken { token in
completion(token)
}
})
service.fetchToken { token in
client.connectUser(userInfo: .init(id: userId), token: token)
}
Development Users
v3:
let client = ChatClient(config: config, tokenProvider: .development(userId))
v4:
let client = ChatClient(config: config)
client.connectUser(userInfo: .init(id: userId), token: .development(userId))
Guest / Anonymous Users
v3:
let client = ChatClient(
config: config,
tokenProvider: .guest(
userId: userId,
name: userName
)
)
// or
let client = ChatClient(config: config, tokenProvider: .anonymous)
v4:
let client = ChatClient(config: config)
client.connectGuestUser(userInfo: .init(id: userId))
// or
client.connectAnonymousUser()
ExtraData Changes
The new 4.0
release changes how extraData
is stored and uses a simpler hashmap-based solution. This approach does not require creating type aliases for all generic classes such as ChatClient
.
Example:
client.connectUser(
userInfo: .init(
id: userCredentials.id,
extraData: ["country": .string("NL")]
),
token: token
)
Message
, User
, Channel
, MessageReaction
models now store extraData
in a [String: RawJSON]
container.
let extraData:[String: RawJSON] = .dictionary([
"name": .string(testPayload.name),
"number": .integer(testPayload.number)
])
Upgrading from ExtraData
If you are using ExtraData
from v3
or before 4.0-beta.8
the steps needed to upgrade are the following:
- Remove all type aliases (
typealias ChatUser = _ChatUser<CustomExtraDataTypes.User>
) - Replace all generic types from
StreamChat
andStreamChatUI
classes (__CurrentChatUserController<T>
->CurrentChatUserController
) with the non-generic version - Remove the extra data structs and either use
extraData
directly or (recommended) extend the models - Update your views to read your custom fields from the
extraData
field
v3:
struct Birthland: UserExtraData {
static var defaultValue = Birthland(birthLand: "")
let birthLand: String
}
v4:
extension ChatUser {
static let birthLandFieldName = "birthLand"
var birthLand: String {
guard let v = extraData[ChatUser.birthLandFieldName] else {
return ""
}
guard case let .string(birthLand) = v else {
return ""
}
return birthLand
}
}
Attachments v4.x Migration
Sending Attachments
In both v4.x
and v3.2
the sequence of steps is the same:
- [1] create a controller for the channel the message should be sent to
- [2] create attachments that should be added to the message
- [3] send the message with attachments using the controller
// [1] Create a controller
let channelController = ChatChannelController(
for: ChannelId(type: .messaging, id: "general")
)
// [2] Create attachments (🚨 has changed in `v4.x`)
let attachments = [
...
]
// [3] Send the message
channelController.createNewMessage(
text: "Hey, have a look at this one",
attachments: attachments,
completion: { result in
// handle the result
print(result)
}
)
Let’s see how to create file/image and custom attachments in v4.x
and make the compiler happy.
File/image Attachments
Version 3.x
// Create an array of `AttachmentEnvelope` objects
let attachments: [AttachmentEnvelope] = [
// Add file attachment by creating `ChatMessageAttachmentSeed`
ChatMessageAttachmentSeed(localURL: fileURL, type: .file),
// Add image attachment by creating `ChatMessageAttachmentSeed`
ChatMessageAttachmentSeed(localURL: imageURL, type: .image)
]
Version 4.x
// Create an array of `AnyAttachmentPayload` objects
let attachments: [AnyAttachmentPayload] = [
// Add file attachment by creating `AnyAttachmentPayload`
try AnyAttachmentPayload(localFileURL: fileURL, attachmentType: .file),
// Add image attachment by creating `AnyAttachmentPayload`
try AnyAttachmentPayload(localFileURL: imageURL, attachmentType: .image)
]
The .file
and .image
attachments are the only built-in attachment types that can be added to the message manually.
Custom Attachments
To add a custom attachments to the message the custom type has to be created first.
This is true for both v3.2
and v4.x
however there are some differences so let’s see what they are:
Version 3.x
// Declare a custom type conforming to `AttachmentEnvelope`
struct Product: AttachmentEnvelope {
let type: AttachmentType = .custom("product")
let name: String
let price: Int
}
// Create a custom attachment
let iPhone = Product(name: "iPhone 12 Pro", price: 999)
// Create an array of `AttachmentEnvelope` objects
let attachments: [AttachmentEnvelope] = [
// Add custom attachment instance directly
iPhone
]
Version 4.x
// Declare a custom type conforming to `AttachmentPayload`
struct Product: AttachmentPayload {
static let type: AttachmentType = "product"
let name: String
let price: Int
}
// Create an attachment payload
let iPhone = Product(name: "iPhone 12 Pro", price: 999)
// Create an array of `AnyAttachmentPayload` objects
let attachments: [AnyAttachmentPayload] = [
// Create `AnyAttachmentPayload` that wraps custom attachment payload
AnyAttachmentPayload(payload: iPhone)
]
Summary
v3.2 | v4.x |
---|---|
An array of AttachmentEnvelope objects is passed when creating a message | An array of AnyAttachmentPayload is passed when creating a message |
Attachments of .file/.image type are added via ChatMessageAttachmentSeed | Attachments of .file/.image type are added via AnyAttachmentPayload |
Custom attachment must conform to AttachmentEnvelope protocol and can be directly passed to createMessage | Custom attachment must conform to AttachmentPayload protocol and wrapped by AnyAttachmentPayload before passing to createMessage |
Get attachments
In both v4.x
and v3.2
the sequence of steps is the same:
- [1] get a
ChatMessage
model - [2] get all attachments of the required type
- [3] access attachment fields
Let’s see how at steps [2] and [3] look in v4.x
for different kind of attachments.
File Attachments
File attachment requires prior uploading before the message is sent. The local state is exposed for .file
attachments to track how the uploading goes.
Version 3.x
// Get `.file` attachments
let fileAttachments = message.attachments
.compactMap { $0 as? ChatMessageDefaultAttachment }
.filter { $0.type == .file }
// Get the first attachment
if let file = fileAttachments.first {
if let localState = file.localState, let localURL = file.localURL {
// Attachment is being uploaded, use local URL to show a preview
print(localState, localURL)
} else if let fileURL = file.url {
// Attachment is uploaded, use remote url
print(fileURL)
}
}
Version 4.x
// Get the first `.file` attachment
if let file = message.fileAttachments.first {
// Show file preview url. If attachment is being uploaded this will be the local URL.
print(file.assetURL)
if let uploadingState = file.uploadingState {
// Attachment is being uploaded, handle uploading progress
print(uploadingState)
}
}
Image Attachments
Image attachment requires prior uploading before the message is sent. The local state is exposed for .image
attachments to track how the uploading goes.
Version 3.x
Image attachments are exposed as ChatMessageDefaultAttachment
. Mandatory fields are optional because of ChatMessageDefaultAttachment
being used for all built-in attachment types.
// Get `.image` attachments
let imageAttachments = message.attachments
.compactMap { $0 as? ChatMessageDefaultAttachment }
.filter { $0.type == .image }
// Get the first attachment
if let image = imageAttachments.first {
if let localState = image.localState, let localURL = image.localURL {
// Attachment is being uploaded, use local URL to show a preview
print(localState, localURL)
} else if let imageURL = image.imageURL {
// Attachment is uploaded, use remote url
print(imageURL)
}
}
Version 4.x
Image attachments are exposed as ChatMessageImageAttachment
. Mandatory fields are non-optional and can be accessed directly on attachment.
// Get the first `.image` attachment
if let image = message.imageAttachments.first {
// Show a preview
print(image.previewURL)
if let uploadingState = image.uploadingState {
// Attachment is being uploaded, handle uploading progress
print(uploadingState)
}
}
Giphy Attachments
The ephemeral message containing giphy attachment will be created when /giphy
command is used.
Version 3.x
Giphy attachments are exposed as ChatMessageDefaultAttachment
. Mandatory fields are optional because of ChatMessageDefaultAttachment
being used for all built-in attachment types.
// Get `.giphy` attachments
let giphyAttachments = message.attachments
.compactMap { $0 as? ChatMessageDefaultAttachment }
.filter { $0.type == .giphy }
// Get the first attachments
if let giphy = giphyAttachments.first {
// Unwrap gif URL
if let gifURL = giphy.imageURL {
// Load and show gif
print(gifURL)
}
}
Version 4.x
Giphy attachments are exposed as ChatMessageGiphyAttachment
. Mandatory fields are non-optional and can be accessed directly on attachment.
// Get the first `.giphy` attachment
if let giphy = message.giphyAttachments.first {
// Load and show gif right away
print(giphy.previewURL)
}
Link Preview Attachments
The link attachment will be added to the message automatically if the message is sent with the text containing the URL.
Version 3.x
Giphy attachments are exposed as ChatMessageDefaultAttachment
. Mandatory fields are optional because of ChatMessageDefaultAttachment
being used for all built-in attachment types.
// Get `.link(...)` attachments
let linkAttachments = message.attachments
.compactMap { $0 as? ChatMessageDefaultAttachment }
.filter { $0.type.isLink }
// Get the first attachment
if let link = linkAttachments.first {
// Unwrap the URL
if let url = link.url {
// Show preview for url
print(url)
}
}
Version 4.x
Link preview attachments are exposed as ChatMessageLinkAttachment
. Mandatory fields are non-optional and can be accessed directly on attachment.
// Get the first `.linkPreview` attachment
if let linkPreview = message.linkAttachments.first {
// Handle the link
print(linkPreview.originalURL)
}
Custom Attachments
Version 3.x
- all built-in attachments exposed as
ChatMessageRawAttachment
- custom payload is stored in
data: Data?
fields - custom data should be decoded manually
data
- attachment
id
is optional
// Custom attachment type has to be `Decodable`
extension Product: Decodable { /* ... */ }
// Get attachments of custom type
let productAttachments = message.attachments
.compactMap { $0 as? ChatMessageRawAttachment }
.filter { $0.type == .custom("product") }
// Get first custom attachment
if let productAttachment = productAttachments.first {
// Unwrap attachment data
if let productData = productAttachment.data {
// Try to decode the custom type from data
let product = try JSONDecoder().decode(Product.self, from: productData)
// Handle custom attachment payload
print(product)
}
}
Version 4.x
- custom attachments are directly accessible on
ChatMessage
- custom payload fields are directly on attachment thanks to
dynamicMemberLookup
- attachment
id
is not-optional 🎉
// It's recommended but not required to create a typealias for custom attachment type to avoid generic stuff
typealias ProductAttachment = _ChatMessageAttachment<Product>
// Get attachments of custom type
let productAttachments = message.attachments(payloadType: Product.self)
// Get first custom attachment
if let product = productAttachments.first {
// Access the payload fields right away
print(product.name)
}
Changes Summary
v3.2 | v4.x |
---|---|
ChatMessage has a single attachments field | ChatMessage has multiple fields one for each attachment type (imageAttachments/giphyAttachments/etc. ) |
Built-in attachments are exposed as ChatMessageDefaultAttachment with mandatory fields being optional | Built-in attachments are exposed as arrays _ChatMessageAttachment<Payload> with concrete payload type with mandatory fields being non-optional |
To be exposed on the message custom attachment must conform to ChatMessageAttachment protocol | In order attachment can be exposed on the message it’s payload must conform to AttachmentPayload protocol |
Custom attachments are exposed as ChatMessageRawAttachment | Custom attachments are exposed the same way built-in attachments are |