Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### 🔄 Changed

# [4.77.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.77.0)
_April 09, 2025_

## StreamChat
### ✅ Added
- Add `ChatChannelController.deletePoll()` for deleting polls [#3632](https://github.com/GetStream/stream-chat-swift/pull/3632)
- Add `ChatChannel.canSendPoll` capability [#3635](https://github.com/GetStream/stream-chat-swift/pull/3635)
- Add `ChatChannel.canCastPollVote` capability [#3635](https://github.com/GetStream/stream-chat-swift/pull/3635)
- Add teams role support for users [#3639](https://github.com/GetStream/stream-chat-swift/pull/3639)
- Add `removeDevice: Bool` parameter to `ChatClient.logout()` [#3640](https://github.com/GetStream/stream-chat-swift/pull/3640)
### 🔄 Changed
- The `ChatClient.logout()` function now automatically removes the user's current device if it has not been removed already [#3640](https://github.com/GetStream/stream-chat-swift/pull/3640)

## StreamChatUI
### 🐞 Fixed
- Fix showing Create Poll action in the composer if the user does not have the capability [#3635](https://github.com/GetStream/stream-chat-swift/pull/3635)
- Fix error when send images with floating point numbers in the original size [#3636](https://github.com/GetStream/stream-chat-swift/pull/3636)

# [4.76.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.76.0)
_March 31, 2025_

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ struct DemoAppConfig {
var shouldShowConnectionBanner: Bool
/// A Boolean value to define if the premium member feature is enabled. This is to test custom member data.
var isPremiumMemberFeatureEnabled: Bool
/// A Boolean value to define if the poll should be deleted when the message is deleted.
var shouldDeletePollOnMessageDeletion: Bool

/// The details to generate expirable tokens in the demo app.
struct TokenRefreshDetails {
Expand Down Expand Up @@ -50,7 +52,8 @@ class AppConfig {
isLocationAttachmentsEnabled: false,
tokenRefreshDetails: nil,
shouldShowConnectionBanner: false,
isPremiumMemberFeatureEnabled: false
isPremiumMemberFeatureEnabled: false,
shouldDeletePollOnMessageDeletion: false
)

if StreamRuntimeCheck.isStreamInternalConfiguration {
Expand All @@ -61,6 +64,7 @@ class AppConfig {
demoAppConfig.isHardDeleteEnabled = true
demoAppConfig.shouldShowConnectionBanner = true
demoAppConfig.isPremiumMemberFeatureEnabled = true
demoAppConfig.shouldDeletePollOnMessageDeletion = true
StreamRuntimeCheck.assertionsEnabled = true
}
}
Expand Down
8 changes: 7 additions & 1 deletion DemoApp/Screens/Create Chat/NameGroupViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class NameGroupViewController: UIViewController {

let avatarView = AvatarView()
let nameLabel = UILabel()
let detailsLabel = UILabel()
let removeButton = UIButton()
let premiumImageView = UIImageView(image: .init(systemName: "crown.fill")!)

Expand All @@ -35,12 +36,17 @@ class NameGroupViewController: UIViewController {
premiumImageView.contentMode = .scaleAspectFill
premiumImageView.tintColor = .systemBlue
premiumImageView.isHidden = true
detailsLabel.isHidden = true
detailsLabel.font = .systemFont(ofSize: 14)

HContainer(spacing: 8, alignment: .center) {
avatarView
.width(30)
.height(30)
nameLabel
VContainer(spacing: 0, alignment: .leading) {
nameLabel
detailsLabel
}
Spacer()
premiumImageView
.width(20)
Expand Down
4 changes: 4 additions & 0 deletions DemoApp/Screens/MembersViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ class MembersViewController: UITableViewController, ChatChannelMemberListControl
Nuke.loadImage(with: imageURL, into: cell.avatarView)
}
cell.nameLabel.text = member.name ?? member.id
if let roles = member.teamsRole {
cell.detailsLabel.text = roles.map { "\($0.key): \($0.value.rawValue)" }.joined(separator: ", ")
cell.detailsLabel.isHidden = false
}
cell.removeButton.isHidden = true
cell.premiumImageView.isHidden = member.isPremium == false || !isPremiumMemberFeatureEnabled
return cell
Expand Down
15 changes: 1 addition & 14 deletions DemoApp/Shared/StreamChatWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,20 +142,7 @@ extension StreamChatWrapper {
return
}

let currentUserController = client.currentUserController()
currentUserController.synchronize()
if let deviceId = currentUserController.currentUser?.currentDevice?.id {
currentUserController.removeDevice(id: deviceId) { error in
if let error = error {
log.error("Removing the device failed with an error \(error)")
}

client.logout(completion: completion)
}
} else {
log.error("No deviceId has been found from the current user.")
client.logout(completion: completion)
}
client.logout(completion: completion)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,25 @@ class DemoComposerVC: ComposerVC {
// )])
}
}

class DemoInputTextView: InputTextView {
override func setUpAppearance() {
backgroundColor = .clear
textContainer.lineFragmentPadding = 8
font = appearance.fonts.body
textColor = appearance.colorPalette.text
textAlignment = .natural
adjustsFontForContentSizeCategory = true

// Calling the layoutManager in debug builds loads a dynamic lib
// Which causes a big performance penalty. So in our Demo App
// we avoid the performance penalty, unless it is using the our internal scheme.
if StreamRuntimeCheck.isStreamInternalConfiguration {
layoutManager.allowsNonContiguousLayout = false
}

placeholderLabel.font = font
placeholderLabel.textColor = appearance.colorPalette.subtitleText
placeholderLabel.adjustsFontSizeToFitWidth = true
}
}
11 changes: 11 additions & 0 deletions DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,17 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
} catch {
self.rootViewController.presentAlert(title: error.localizedDescription)
}
}),
.init(title: "Add a team role for the current user", isEnabled: true, handler: { [unowned self] _ in
self.rootViewController.presentAlert(title: "Enter the team role", textFieldPlaceholder: "Enter role") { role in
if let role, !role.isEmpty {
client.currentUserController().updateUserData(teamsRole: ["ios": role]) { error in
if let error {
log.error("Couldn't add role to custom team for the current user: \(error)")
}
}
}
}
})
]

Expand Down
26 changes: 26 additions & 0 deletions DemoApp/StreamChat/Components/DemoChatMessageActionsVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,32 @@ final class DemoChatMessageActionsVC: ChatMessageActionsVC {

return actions
}

override func deleteActionItem() -> ChatMessageActionItem {
DeleteActionItem(
action: { [weak self] _ in
guard let self = self else { return }
self.alertsRouter.showMessageDeletionConfirmationAlert { confirmed in
guard confirmed else { return }

self.messageController.deleteMessage { _ in
let pollId = self.messageController.message?.poll?.id
if let pollId, AppConfig.shared.demoAppConfig.shouldDeletePollOnMessageDeletion {
let channelController = self.messageController.client.channelController(
for: self.messageController.cid
)
channelController.deletePoll(pollId: pollId) { _ in
self.delegate?.chatMessageActionsVCDidFinish(self)
}
} else {
self.delegate?.chatMessageActionsVCDidFinish(self)
}
}
}
},
appearance: appearance
)
}

func pinMessageActionItem() -> PinMessageActionItem {
PinMessageActionItem(
Expand Down
1 change: 1 addition & 0 deletions DemoApp/StreamChat/StreamChatWrapper+DemoApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ extension StreamChatWrapper {
Components.default.messageListVC = DemoChatMessageListVC.self
Components.default.quotedMessageView = DemoQuotedChatMessageView.self
Components.default.messageComposerVC = DemoComposerVC.self
Components.default.inputTextView = DemoInputTextView.self
Components.default.channelContentView = DemoChatChannelListItemView.self
Components.default.channelListRouter = DemoChatChannelListRouter.self
Components.default.channelVC = DemoChatChannelVC.self
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class CurrentUserPayload: UserPayload {
name: String?,
imageURL: URL?,
role: UserRole,
teamsRole: [String: UserRole]?,
createdAt: Date,
updatedAt: Date,
deactivatedAt: Date?,
Expand Down Expand Up @@ -53,6 +54,7 @@ class CurrentUserPayload: UserPayload {
name: name,
imageURL: imageURL,
role: role,
teamsRole: teamsRole,
createdAt: createdAt,
updatedAt: updatedAt,
deactivatedAt: deactivatedAt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ enum UserPayloadsCodingKeys: String, CodingKey, CaseIterable {
case language
case privacySettings = "privacy_settings"
case blockedUserIds = "blocked_user_ids"
case teamsRole = "teams_role"
}

// MARK: - GET users
Expand All @@ -38,6 +39,7 @@ class UserPayload: Decodable {
let name: String?
let imageURL: URL?
let role: UserRole
let teamsRole: [String: UserRole]?
let createdAt: Date
let updatedAt: Date
let deactivatedAt: Date?
Expand All @@ -54,6 +56,7 @@ class UserPayload: Decodable {
name: String?,
imageURL: URL?,
role: UserRole,
teamsRole: [String: UserRole]?,
createdAt: Date,
updatedAt: Date,
deactivatedAt: Date?,
Expand All @@ -69,6 +72,7 @@ class UserPayload: Decodable {
self.name = name
self.imageURL = imageURL
self.role = role
self.teamsRole = teamsRole
self.createdAt = createdAt
self.updatedAt = updatedAt
self.deactivatedAt = deactivatedAt
Expand All @@ -88,6 +92,7 @@ class UserPayload: Decodable {
name = try container.decodeIfPresent(String.self, forKey: .name)
imageURL = try container.decodeIfPresent(String.self, forKey: .imageURL).flatMap(URL.init(string:))
role = try container.decode(UserRole.self, forKey: .role)
teamsRole = try container.decodeIfPresent([String: UserRole].self, forKey: .teamsRole)
createdAt = try container.decode(Date.self, forKey: .createdAt)
updatedAt = try container.decode(Date.self, forKey: .updatedAt)
deactivatedAt = try container.decodeIfPresent(Date.self, forKey: .deactivatedAt)
Expand Down Expand Up @@ -168,19 +173,22 @@ struct UserUpdateRequestBody: Encodable {
let privacySettings: UserPrivacySettingsPayload?
let role: UserRole?
let extraData: [String: RawJSON]?
let teamsRole: [String: String]?

init(
name: String?,
imageURL: URL?,
privacySettings: UserPrivacySettingsPayload?,
role: UserRole?,
teamsRole: [String: String]?,
extraData: [String: RawJSON]?
) {
self.name = name
self.imageURL = imageURL
self.privacySettings = privacySettings
self.role = role
self.extraData = extraData
self.teamsRole = teamsRole
}

func encode(to encoder: Encoder) throws {
Expand All @@ -189,6 +197,7 @@ struct UserUpdateRequestBody: Encodable {
try container.encodeIfPresent(imageURL, forKey: .imageURL)
try container.encodeIfPresent(privacySettings, forKey: .privacySettings)
try container.encodeIfPresent(role, forKey: .role)
try container.encodeIfPresent(teamsRole, forKey: .teamsRole)
try extraData?.encode(to: encoder)
}
}
Expand Down
26 changes: 22 additions & 4 deletions Sources/StreamChat/ChatClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -483,15 +483,33 @@ public class ChatClient {
}
}

/// Disconnects the chat client form the chat servers and removes all the local data related.
/// Disconnects the chat client from the chat servers and removes all the local data related.
@available(*, deprecated, message: "Use the asynchronous version of `logout` for increased safety")
public func logout() {
logout {}
}

/// Disconnects the chat client from the chat servers and removes all the local data related.
public func logout(completion: @escaping () -> Void) {
authenticationRepository.logOutUser()
/// - Parameters:
/// - removeDevice: If `true`, it removes the current device from the user's registered devices automatically.
/// By default it is enabled.
public func logout(
removeDevice: Bool = true,
completion: @escaping () -> Void
) {
let currentUserController = currentUserController()
if removeDevice, let currentUserDevice = currentUserController.currentUser?.currentDevice {
currentUserController.removeDevice(id: currentUserDevice.id) { [weak self] error in
if let error {
log.error(error)
}
self?.authenticationRepository.logOutUser()
}
}

if !removeDevice {
authenticationRepository.logOutUser()
}

// Stop tracking active components
syncRepository.removeAllTracked()
Expand Down Expand Up @@ -646,7 +664,7 @@ public class ChatClient {

extension ChatClient: AuthenticationRepositoryDelegate {
func logOutUser(completion: @escaping () -> Void) {
logout(completion: completion)
logout(removeDevice: false, completion: completion)
}

func didFinishSettingUpAuthenticationEnvironment(for state: EnvironmentState) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,18 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
}
}
}

/// Deletes the poll with the provided poll id.
/// - Parameters:
/// - pollId: The id of the poll to be deleted.
/// - completion: A closure to be executed once the poll is deleted, returning either an `Error` on failure or `nil` on success.
public func deletePoll(pollId: String, completion: ((Error?) -> Void)? = nil) {
pollsRepository.deletePoll(pollId: pollId) { [weak self] error in
self?.callback {
completion?(error)
}
}
}

/// Add users to the channel as members with additional data.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ public extension CurrentChatUserController {
/// - imageURL: Optionally provide a new image to be updated.
/// - privacySettings: The privacy settings of the user. Example: If the user does not want to expose typing events or read events.
/// - role: The role for the user.
/// - teamsRole: The role for the user in a specific team. Example: `["teamId": "role"]`.
/// - userExtraData: Optionally provide new user extra data to be updated.
/// - unset: Existing values for specified properties are removed. For example, `image` or `name`.
/// - completion: Called when user is successfuly updated, or with error.
Expand All @@ -200,6 +201,7 @@ public extension CurrentChatUserController {
imageURL: URL? = nil,
privacySettings: UserPrivacySettings? = nil,
role: UserRole? = nil,
teamsRole: [String: String]? = nil,
userExtraData: [String: RawJSON] = [:],
unsetProperties: Set<String> = [],
completion: ((Error?) -> Void)? = nil
Expand All @@ -215,6 +217,7 @@ public extension CurrentChatUserController {
imageURL: imageURL,
privacySettings: privacySettings,
role: role,
teamsRole: teamsRole,
userExtraData: userExtraData
) { error in
self.callback {
Expand Down
1 change: 1 addition & 0 deletions Sources/StreamChat/Database/DTOs/CurrentUserDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ extension CurrentChatUser {
isInvisible: dto.isInvisible,
isBanned: user.isBanned,
userRole: UserRole(rawValue: user.userRoleRaw),
teamsRole: user.teamsRole?.mapValues { UserRole(rawValue: $0) },
createdAt: user.userCreatedAt.bridgeDate,
updatedAt: user.userUpdatedAt.bridgeDate,
deactivatedAt: user.userDeactivatedAt?.bridgeDate,
Expand Down
1 change: 1 addition & 0 deletions Sources/StreamChat/Database/DTOs/MemberModelDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ extension ChatChannelMember {
isBanned: dto.user.isBanned,
isFlaggedByCurrentUser: dto.user.flaggedBy != nil,
userRole: UserRole(rawValue: dto.user.userRoleRaw),
teamsRole: dto.user.teamsRole?.mapValues { UserRole(rawValue: $0) },
userCreatedAt: dto.user.userCreatedAt.bridgeDate,
userUpdatedAt: dto.user.userUpdatedAt.bridgeDate,
deactivatedAt: dto.user.userDeactivatedAt?.bridgeDate,
Expand Down
6 changes: 6 additions & 0 deletions Sources/StreamChat/Database/DTOs/PollDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -246,4 +246,10 @@ extension NSManagedObjectContext {
func poll(id: String) throws -> PollDTO? {
PollDTO.load(pollId: id, context: self)
}

func deletePoll(pollId: String) throws -> PollDTO? {
guard let poll = try poll(id: pollId) else { return nil }
delete(poll)
return poll
}
}
Loading
Loading