From 1b45a9e04720f0e5f89053db7d9c52c849a1b90e Mon Sep 17 00:00:00 2001 From: Toomas Vahter Date: Wed, 19 Aug 2026 14:30:31 +0300 Subject: [PATCH] Use generated ChannelCapabilities, VotingVisibility and PushPreferenceLevel --- Scripts/openapi_generate.sh | 57 +++++++----- .../ChannelDetailPayload+Extensions.swift | 2 +- .../ChannelController/ChannelController.swift | 4 +- .../CurrentUserController.swift | 2 +- .../PollController/PollController.swift | 14 --- .../StreamChat/Database/DTOs/ChannelDTO.swift | 2 +- .../OpenAPI/models/BlockedUserResponse.swift | 2 + .../OpenAPI/models/ChannelCapability.swift | 56 +++++++++++ .../OpenAPI/models/ChannelDetailPayload.swift | 8 +- .../OpenAPI/models/ChannelMemberRequest.swift | 1 + .../OpenAPI/models/ChannelOwnCapability.swift | 61 ------------ .../OpenAPI/models/CreateDeviceRequest.swift | 27 +++--- .../models/CreatePollRequestBody.swift | 27 +++--- .../models/DeleteChannelResponse.swift | 1 + .../OpenAPI/models/MemberPayload.swift | 1 + .../models/MessageReactionPayload.swift | 1 + .../OpenAPI/models/MutedChannelPayload.swift | 2 + .../OpenAPI/models/PollPayload.swift | 1 + .../OpenAPI/models/PollVotePayload.swift | 1 + .../OpenAPI/models/PushPreferenceInput.swift | 37 ++++---- .../models/UpdateLiveLocationRequest.swift | 8 +- Sources/StreamChat/Models/Channel.swift | 92 +------------------ .../Models/ChannelCapability+Extensions.swift | 21 +++++ .../ChannelPayload+asModel.swift | 2 +- .../PushPreferences/PushPreferenceLevel.swift | 38 +------- .../Repositories/PollsRepository.swift | 4 +- .../Workers/CurrentUserUpdater.swift | 2 +- .../StreamChat/Workers/MessageUpdater.swift | 4 +- .../Database/DTOs/ChannelDTO_Tests.swift | 17 ++++ .../Models/ChatChannel_Tests.swift | 20 +--- .../Workers/CurrentUserUpdater_Tests.swift | 2 +- 31 files changed, 202 insertions(+), 315 deletions(-) create mode 100644 Sources/StreamChat/Generated/OpenAPI/models/ChannelCapability.swift delete mode 100644 Sources/StreamChat/Generated/OpenAPI/models/ChannelOwnCapability.swift create mode 100644 Sources/StreamChat/Models/ChannelCapability+Extensions.swift diff --git a/Scripts/openapi_generate.sh b/Scripts/openapi_generate.sh index d959a3e7441..687d38425c8 100755 --- a/Scripts/openapi_generate.sh +++ b/Scripts/openapi_generate.sh @@ -191,6 +191,7 @@ rm -rf "$OUTPUT_DIR_CHAT" ./build/chat-manager openapi generate-client --language swift \ --opt immutable_models=true --opt access_modifier=internal \ --opt encodable_filter_conditions=true \ + --opt struct_over_enum=true \ --spec ./releases/v2/chat-clientside-api.yaml --output "$OUTPUT_DIR_CHAT" ) # 2. Drop the generated async API client — the SDK ships its own APIClient. @@ -398,33 +399,19 @@ rename_generated ReactionResponse MessageReactionPayload rename_generated_type QueryReactionsResponse MessageReactionsPayload rename_generated ChannelMemberResponse MemberPayload rename_generated ChannelMute MutedChannelPayload +rename_generated ChannelOwnCapability ChannelCapability rename_generated ChannelResponse ChannelDetailPayload rename_generated MuteChannelResponse MutedChannelPayloadResponse +rename_generated_type CreatePollRequestVotingVisibility VotingVisibility +rename_generated_type PushPreferenceInputChatLevel PushPreferenceLevel + rename_generated_type HideChannelResponse EmptyResponse rename_generated_type MarkDeliveredResponse EmptyResponse rename_generated_type Response EmptyResponse rename_generated_type ShowChannelResponse EmptyResponse rename_generated_type UnmuteResponse EmptyResponse -# Remove a generated property (declaration, doc comment, init param, assignment, -# CodingKeys case). Runs before publicize, so there are no access modifiers to -# handle. Assumes the single-line init the generator emits (step 7 re-wraps). -remove_property() { - local file="$OUTPUT_DIR_CHAT/models/$1.swift" - awk -v p="$2" ' - function flush() { for (i = 1; i <= n; i++) print b[i]; n = 0 } - { s = $0; sub(/^[[:space:]]+/, "", s) } - s ~ /^(\/\/\/|@available)/ { b[++n] = $0; next } - s ~ "^let " p ": " { n = 0; next } - s ~ "^self\\." p " = " p "$" { next } - s ~ "^case " p "( =|$)" { next } - s ~ /^init\(/ { sub("\\(" p ": [^,)]*, ", "("); sub(", " p ": [^,)]*", ""); sub("\\(" p ": [^,)]*\\)", "()") } - { flush(); print } - ' "$file" > "$file.tmp" && mv "$file.tmp" "$file" -} -remove_property FileUploadResponse duration - retype_property PushPreference chatLevel String PushPreferenceLevel rename_property PushPreference chatLevel level restore_nonoptional_property PushPreference level PushPreferenceLevel .all @@ -460,6 +447,7 @@ remove_property() { perl -0777 -pi -e 's/ &&(\n\s*\})/$1/g' "$file" } remove_property CurrentUserUnreads duration +remove_property FileUploadResponse duration remove_property MessageReactionsPayload duration remove_property PushPreferenceInput callLevel remove_property PushPreferenceInput chatPreferences @@ -485,17 +473,17 @@ retype_property ChannelDetailPayload config ChannelConfigWithInfo ChannelConfig # Will be changed on the generation side later require_property ChannelDetailPayload config -remove_nested_enum() { +remove_type() { local file="$OUTPUT_DIR_CHAT/models/$1.swift" awk -v e="$2" ' - $0 ~ "^ enum " e ":" { skip = 1; next } - skip && /^ }$/ { skip = 0; next } - skip { next } + $0 ~ "^(enum|struct) " e ":" { skip = 1; next } + skip && /^}$/ { skip = 0; next } + skip { next } { print } ' "$file" > "$file.tmp" && mv "$file.tmp" "$file" } -remove_nested_enum PushPreferenceInput PushPreferenceInputCallLevel -remove_nested_enum PushPreferenceInput PushPreferenceInputFeedsLevel +remove_type PushPreferenceInput PushPreferenceInputCallLevel +remove_type PushPreferenceInput PushPreferenceInputFeedsLevel # 4c. Expose selected generated models as public API. The class and its stored # properties become public, along with the generated Hashable conformance @@ -522,6 +510,27 @@ publicize_model UploadConfig publicize_model UserGroup publicize_model UserGroupMember +# Expose a generated RawRepresentable struct as public API. Unlike publicize_model, the +# init must be public too — it is the RawRepresentable requirement — along with every +# static let holding a known value. The struct is looked up by name, since the file +# named after a model also holds the structs generated for its string properties. +publicize_raw_representable() { + local file="$OUTPUT_DIR_CHAT/models/$1.swift" + awk -v n="${2:-$1}" ' + $0 ~ "^struct " n ":" { sub(/^struct /, "public struct "); inside = 1; print; next } + inside && /^}$/ { inside = 0; print; next } + inside { + sub(/^ let /, " public let ") + sub(/^ init\(/, " public init(") + sub(/^ static let /, " public static let ") + } + { print } + ' "$file" > "$file.tmp" && mv "$file.tmp" "$file" +} +publicize_raw_representable ChannelCapability +publicize_raw_representable CreatePollRequestBody VotingVisibility +publicize_raw_representable PushPreferenceInput PushPreferenceLevel + # Drop `final` from a generated model so hand-written payloads can subclass it. unfinalize_model() { local file="$OUTPUT_DIR_CHAT/models/$1.swift" diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelDetailPayload+Extensions.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelDetailPayload+Extensions.swift index 57e6db4166c..57358909e88 100644 --- a/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelDetailPayload+Extensions.swift +++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelDetailPayload+Extensions.swift @@ -76,7 +76,7 @@ extension ChannelDetailPayload { memberCount: memberCount, members: members, messageCount: messageCount, - ownCapabilities: ownCapabilities?.compactMap(ChannelOwnCapability.init(rawValue:)), + ownCapabilities: ownCapabilities?.map(ChannelCapability.init(rawValue:)), team: team, truncatedAt: truncatedAt, truncatedBy: truncatedBy, diff --git a/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift b/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift index 7cdfc5ed40f..b86f6712f66 100644 --- a/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift +++ b/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift @@ -1146,7 +1146,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP description: description, enforceUniqueVote: enforceUniqueVote, maxVotesAllowed: maxVotesAllowed, - votingVisibility: votingVisibility?.rawValue, + votingVisibility: votingVisibility, options: options, custom: extraData ) { [weak self] result in @@ -1877,7 +1877,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP let channelPreference = PushPreferenceInput( channelCid: channelId.rawValue, - chatLevel: PushPreferenceInput.PushPreferenceInputChatLevel(rawValue: level.rawValue), + chatLevel: level, removeDisable: true ) diff --git a/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift b/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift index d73249ba44d..15965832873 100644 --- a/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift +++ b/Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift @@ -477,7 +477,7 @@ public extension CurrentChatUserController { completion: (@MainActor (Result) -> Void)? = nil ) { let userPreference = PushPreferenceInput( - chatLevel: PushPreferenceInput.PushPreferenceInputChatLevel(rawValue: level.rawValue), + chatLevel: level, removeDisable: true ) diff --git a/Sources/StreamChat/Controllers/PollController/PollController.swift b/Sources/StreamChat/Controllers/PollController/PollController.swift index 53ea5182764..c85b5b6da28 100644 --- a/Sources/StreamChat/Controllers/PollController/PollController.swift +++ b/Sources/StreamChat/Controllers/PollController/PollController.swift @@ -274,20 +274,6 @@ public class PollController: DataController, DelegateCallable, DataStoreProvider } } -/// Represents the visibility of votes in a poll. -public struct VotingVisibility: RawRepresentable, Equatable, Sendable { - public let rawValue: String - - public init(rawValue: String) { - self.rawValue = rawValue - } - - /// Votes are public and can be seen by everyone. - public static let `public` = Self(rawValue: "public") - /// Votes are anonymous and cannot be attributed to individual users. - public static let anonymous = Self(rawValue: "anonymous") -} - extension PollController { struct Environment { var pollObserverBuilder: ( diff --git a/Sources/StreamChat/Database/DTOs/ChannelDTO.swift b/Sources/StreamChat/Database/DTOs/ChannelDTO.swift index cabaaf43a66..e4c6aed50ba 100644 --- a/Sources/StreamChat/Database/DTOs/ChannelDTO.swift +++ b/Sources/StreamChat/Database/DTOs/ChannelDTO.swift @@ -682,7 +682,7 @@ extension ChatChannel { createdBy: dto.createdBy?.asModel(), config: dto.config.asModel(), filterTags: Set(dto.filterTags.map(\.name)), - ownCapabilities: Set(dto.ownCapabilities.compactMap(ChannelCapability.init(rawValue:))), + ownCapabilities: Set(dto.ownCapabilities.map(ChannelCapability.init(rawValue:))), isFrozen: dto.isFrozen, isDisabled: dto.isDisabled, isBlocked: dto.isBlocked, diff --git a/Sources/StreamChat/Generated/OpenAPI/models/BlockedUserResponse.swift b/Sources/StreamChat/Generated/OpenAPI/models/BlockedUserResponse.swift index fe6d2f98c3e..faa5d368485 100644 --- a/Sources/StreamChat/Generated/OpenAPI/models/BlockedUserResponse.swift +++ b/Sources/StreamChat/Generated/OpenAPI/models/BlockedUserResponse.swift @@ -5,10 +5,12 @@ import Foundation final class BlockedUserResponse: Sendable, Codable, JSONEncodable { + /// User response object let blockedUser: UserPayload /// ID of the user who got blocked let blockedUserId: String let createdAt: Date + /// User response object let user: UserPayload /// ID of the user who blocked another user let userId: String diff --git a/Sources/StreamChat/Generated/OpenAPI/models/ChannelCapability.swift b/Sources/StreamChat/Generated/OpenAPI/models/ChannelCapability.swift new file mode 100644 index 00000000000..fa521d11950 --- /dev/null +++ b/Sources/StreamChat/Generated/OpenAPI/models/ChannelCapability.swift @@ -0,0 +1,56 @@ +// +// Copyright © 2026 Stream.io Inc. All rights reserved. +// + +import Foundation + +public struct ChannelCapability: RawRepresentable, Codable, Hashable, Sendable { + public let rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + public static let banChannelMembers = Self(rawValue: "ban-channel-members") + public static let castPollVote = Self(rawValue: "cast-poll-vote") + public static let connectEvents = Self(rawValue: "connect-events") + public static let createAttachment = Self(rawValue: "create-attachment") + public static let createMention = Self(rawValue: "create-mention") + public static let deleteAnyMessage = Self(rawValue: "delete-any-message") + public static let deleteChannel = Self(rawValue: "delete-channel") + public static let deleteOwnMessage = Self(rawValue: "delete-own-message") + public static let deliveryEvents = Self(rawValue: "delivery-events") + public static let flagMessage = Self(rawValue: "flag-message") + public static let freezeChannel = Self(rawValue: "freeze-channel") + public static let joinChannel = Self(rawValue: "join-channel") + public static let leaveChannel = Self(rawValue: "leave-channel") + public static let muteChannel = Self(rawValue: "mute-channel") + public static let notifyChannel = Self(rawValue: "notify-channel") + public static let notifyGroup = Self(rawValue: "notify-group") + public static let notifyHere = Self(rawValue: "notify-here") + public static let notifyRole = Self(rawValue: "notify-role") + public static let pinMessage = Self(rawValue: "pin-message") + public static let queryPollVotes = Self(rawValue: "query-poll-votes") + public static let quoteMessage = Self(rawValue: "quote-message") + public static let readEvents = Self(rawValue: "read-events") + public static let searchMessages = Self(rawValue: "search-messages") + public static let sendCustomEvents = Self(rawValue: "send-custom-events") + public static let sendLinks = Self(rawValue: "send-links") + public static let sendMessage = Self(rawValue: "send-message") + public static let sendPoll = Self(rawValue: "send-poll") + public static let sendReaction = Self(rawValue: "send-reaction") + public static let sendReply = Self(rawValue: "send-reply") + public static let sendRestrictedVisibilityMessage = Self(rawValue: "send-restricted-visibility-message") + public static let sendTypingEvents = Self(rawValue: "send-typing-events") + public static let setChannelCooldown = Self(rawValue: "set-channel-cooldown") + public static let shareLocation = Self(rawValue: "share-location") + public static let skipSlowMode = Self(rawValue: "skip-slow-mode") + public static let slowMode = Self(rawValue: "slow-mode") + public static let typingEvents = Self(rawValue: "typing-events") + public static let updateAnyMessage = Self(rawValue: "update-any-message") + public static let updateChannel = Self(rawValue: "update-channel") + public static let updateChannelMembers = Self(rawValue: "update-channel-members") + public static let updateOwnMessage = Self(rawValue: "update-own-message") + public static let updateThread = Self(rawValue: "update-thread") + public static let uploadFile = Self(rawValue: "upload-file") +} diff --git a/Sources/StreamChat/Generated/OpenAPI/models/ChannelDetailPayload.swift b/Sources/StreamChat/Generated/OpenAPI/models/ChannelDetailPayload.swift index 2af391e999f..52e455a65d1 100644 --- a/Sources/StreamChat/Generated/OpenAPI/models/ChannelDetailPayload.swift +++ b/Sources/StreamChat/Generated/OpenAPI/models/ChannelDetailPayload.swift @@ -18,6 +18,7 @@ final class ChannelDetailPayload: Sendable, Codable, JSONEncodable { let cooldown: Int? /// Date/time of creation let createdAt: Date + /// User response object let createdBy: UserPayload? /// Custom data for this object let custom: [String: RawJSON] @@ -47,11 +48,12 @@ final class ChannelDetailPayload: Sendable, Codable, JSONEncodable { /// Whether this channel is muted or not let muted: Bool? /// List of channel capabilities of authenticated user - let ownCapabilities: [ChannelOwnCapability]? + let ownCapabilities: [ChannelCapability]? /// Team the channel belongs to (multi-tenant only) let team: String? /// Date of the latest truncation of the channel let truncatedAt: Date? + /// User response object let truncatedBy: UserPayload? /// Type of the channel let type: String @@ -81,7 +83,7 @@ final class ChannelDetailPayload: Sendable, Codable, JSONEncodable { messageCount: Int? = nil, muteExpiresAt: Date? = nil, muted: Bool? = nil, - ownCapabilities: [ChannelOwnCapability]? = nil, + ownCapabilities: [ChannelCapability]? = nil, team: String? = nil, truncatedAt: Date? = nil, truncatedBy: UserPayload? = nil, @@ -183,7 +185,7 @@ final class ChannelDetailPayload: Sendable, Codable, JSONEncodable { messageCount = try container.decodeIfPresent(Int.self, forKey: .messageCount) muteExpiresAt = try container.decodeIfPresent(Date.self, forKey: .muteExpiresAt) muted = try container.decodeIfPresent(Bool.self, forKey: .muted) - ownCapabilities = try container.decodeIfPresent([ChannelOwnCapability].self, forKey: .ownCapabilities) + ownCapabilities = try container.decodeIfPresent([ChannelCapability].self, forKey: .ownCapabilities) team = try container.decodeIfPresent(String.self, forKey: .team) truncatedAt = try container.decodeIfPresent(Date.self, forKey: .truncatedAt) truncatedBy = try container.decodeIfPresent(UserPayload.self, forKey: .truncatedBy) diff --git a/Sources/StreamChat/Generated/OpenAPI/models/ChannelMemberRequest.swift b/Sources/StreamChat/Generated/OpenAPI/models/ChannelMemberRequest.swift index 393cb1b6215..2217d49302a 100644 --- a/Sources/StreamChat/Generated/OpenAPI/models/ChannelMemberRequest.swift +++ b/Sources/StreamChat/Generated/OpenAPI/models/ChannelMemberRequest.swift @@ -8,6 +8,7 @@ final class ChannelMemberRequest: Sendable, Codable, JSONEncodable { /// Role of the member in the channel let channelRole: String? let custom: [String: RawJSON]? + /// User response object let user: UserPayload? let userId: String diff --git a/Sources/StreamChat/Generated/OpenAPI/models/ChannelOwnCapability.swift b/Sources/StreamChat/Generated/OpenAPI/models/ChannelOwnCapability.swift deleted file mode 100644 index a24b63c9b1e..00000000000 --- a/Sources/StreamChat/Generated/OpenAPI/models/ChannelOwnCapability.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// Copyright © 2026 Stream.io Inc. All rights reserved. -// - -import Foundation - -enum ChannelOwnCapability: String, Sendable, Codable, CaseIterable { - case banChannelMembers = "ban-channel-members" - case castPollVote = "cast-poll-vote" - case connectEvents = "connect-events" - case createAttachment = "create-attachment" - case createMention = "create-mention" - case deleteAnyMessage = "delete-any-message" - case deleteChannel = "delete-channel" - case deleteOwnMessage = "delete-own-message" - case deliveryEvents = "delivery-events" - case flagMessage = "flag-message" - case freezeChannel = "freeze-channel" - case joinChannel = "join-channel" - case leaveChannel = "leave-channel" - case muteChannel = "mute-channel" - case notifyChannel = "notify-channel" - case notifyGroup = "notify-group" - case notifyHere = "notify-here" - case notifyRole = "notify-role" - case pinMessage = "pin-message" - case queryPollVotes = "query-poll-votes" - case quoteMessage = "quote-message" - case readEvents = "read-events" - case searchMessages = "search-messages" - case sendCustomEvents = "send-custom-events" - case sendLinks = "send-links" - case sendMessage = "send-message" - case sendPoll = "send-poll" - case sendReaction = "send-reaction" - case sendReply = "send-reply" - case sendRestrictedVisibilityMessage = "send-restricted-visibility-message" - case sendTypingEvents = "send-typing-events" - case setChannelCooldown = "set-channel-cooldown" - case shareLocation = "share-location" - case skipSlowMode = "skip-slow-mode" - case slowMode = "slow-mode" - case typingEvents = "typing-events" - case updateAnyMessage = "update-any-message" - case updateChannel = "update-channel" - case updateChannelMembers = "update-channel-members" - case updateOwnMessage = "update-own-message" - case updateThread = "update-thread" - case uploadFile = "upload-file" - case unknown = "_unknown" - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let decodedValue = try? container.decode(String.self), - let value = ChannelOwnCapability(rawValue: decodedValue) { - self = value - } else { - self = .unknown - } - } -} diff --git a/Sources/StreamChat/Generated/OpenAPI/models/CreateDeviceRequest.swift b/Sources/StreamChat/Generated/OpenAPI/models/CreateDeviceRequest.swift index f11ba0b32b2..4b886ffe075 100644 --- a/Sources/StreamChat/Generated/OpenAPI/models/CreateDeviceRequest.swift +++ b/Sources/StreamChat/Generated/OpenAPI/models/CreateDeviceRequest.swift @@ -4,25 +4,20 @@ import Foundation -final class CreateDeviceRequest: Sendable, Codable, JSONEncodable { - enum CreateDeviceRequestPushProvider: String, Sendable, Codable, CaseIterable { - case apn - case firebase - case huawei - case xiaomi - case unknown = "_unknown" +struct CreateDeviceRequestPushProvider: RawRepresentable, Codable, Hashable, Sendable { + let rawValue: String - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let decodedValue = try? container.decode(String.self), - let value = Self(rawValue: decodedValue) { - self = value - } else { - self = .unknown - } - } + init(rawValue: String) { + self.rawValue = rawValue } + static let apn = Self(rawValue: "apn") + static let firebase = Self(rawValue: "firebase") + static let huawei = Self(rawValue: "huawei") + static let xiaomi = Self(rawValue: "xiaomi") +} + +final class CreateDeviceRequest: Sendable, Codable, JSONEncodable { /// Stable physical device identifier used to deduplicate pushes across push providers (e.g. APNs VoIP and Firebase on the same iOS device). Distinct from 'id', which is the push token. let hardwareId: String? /// Device ID diff --git a/Sources/StreamChat/Generated/OpenAPI/models/CreatePollRequestBody.swift b/Sources/StreamChat/Generated/OpenAPI/models/CreatePollRequestBody.swift index 6df4248a772..8f646d75055 100644 --- a/Sources/StreamChat/Generated/OpenAPI/models/CreatePollRequestBody.swift +++ b/Sources/StreamChat/Generated/OpenAPI/models/CreatePollRequestBody.swift @@ -4,23 +4,18 @@ import Foundation -final class CreatePollRequestBody: Sendable, Codable, JSONEncodable { - enum CreatePollRequestVotingVisibility: String, Sendable, Codable, CaseIterable { - case `public` - case anonymous - case unknown = "_unknown" +public struct VotingVisibility: RawRepresentable, Codable, Hashable, Sendable { + public let rawValue: String - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let decodedValue = try? container.decode(String.self), - let value = Self(rawValue: decodedValue) { - self = value - } else { - self = .unknown - } - } + public init(rawValue: String) { + self.rawValue = rawValue } + public static let `public` = Self(rawValue: "public") + public static let anonymous = Self(rawValue: "anonymous") +} + +final class CreatePollRequestBody: Sendable, Codable, JSONEncodable { /// Indicates whether users can suggest user defined answers let allowAnswers: Bool? let allowUserSuggestedOptions: Bool? @@ -38,7 +33,7 @@ final class CreatePollRequestBody: Sendable, Codable, JSONEncodable { /// The name of the poll let name: String let options: [PollOptionRequestBody]? - let votingVisibility: CreatePollRequestVotingVisibility? + let votingVisibility: VotingVisibility? init( allowAnswers: Bool? = nil, @@ -51,7 +46,7 @@ final class CreatePollRequestBody: Sendable, Codable, JSONEncodable { maxVotesAllowed: Int? = nil, name: String, options: [PollOptionRequestBody]? = nil, - votingVisibility: CreatePollRequestVotingVisibility? = nil + votingVisibility: VotingVisibility? = nil ) { self.allowAnswers = allowAnswers self.allowUserSuggestedOptions = allowUserSuggestedOptions diff --git a/Sources/StreamChat/Generated/OpenAPI/models/DeleteChannelResponse.swift b/Sources/StreamChat/Generated/OpenAPI/models/DeleteChannelResponse.swift index 4e2fca6fe28..aed8f214010 100644 --- a/Sources/StreamChat/Generated/OpenAPI/models/DeleteChannelResponse.swift +++ b/Sources/StreamChat/Generated/OpenAPI/models/DeleteChannelResponse.swift @@ -5,6 +5,7 @@ import Foundation final class DeleteChannelResponse: Sendable, Codable, JSONEncodable { + /// Represents channel in chat let channel: ChannelDetailPayload? init(channel: ChannelDetailPayload? = nil) { diff --git a/Sources/StreamChat/Generated/OpenAPI/models/MemberPayload.swift b/Sources/StreamChat/Generated/OpenAPI/models/MemberPayload.swift index 61b17a520eb..4fbc1877ff6 100644 --- a/Sources/StreamChat/Generated/OpenAPI/models/MemberPayload.swift +++ b/Sources/StreamChat/Generated/OpenAPI/models/MemberPayload.swift @@ -38,6 +38,7 @@ final class MemberPayload: Sendable, Codable, JSONEncodable { let status: String? /// Date/time of the last update let updatedAt: Date + /// User response object let user: UserPayload? let userId: String? diff --git a/Sources/StreamChat/Generated/OpenAPI/models/MessageReactionPayload.swift b/Sources/StreamChat/Generated/OpenAPI/models/MessageReactionPayload.swift index d76141fe19b..a833ea40846 100644 --- a/Sources/StreamChat/Generated/OpenAPI/models/MessageReactionPayload.swift +++ b/Sources/StreamChat/Generated/OpenAPI/models/MessageReactionPayload.swift @@ -17,6 +17,7 @@ final class MessageReactionPayload: Sendable, Codable, JSONEncodable { let type: MessageReactionType /// Date/time of the last update let updatedAt: Date + /// User response object let user: UserPayload /// User ID let userId: String diff --git a/Sources/StreamChat/Generated/OpenAPI/models/MutedChannelPayload.swift b/Sources/StreamChat/Generated/OpenAPI/models/MutedChannelPayload.swift index 93732db4688..d1b3847e989 100644 --- a/Sources/StreamChat/Generated/OpenAPI/models/MutedChannelPayload.swift +++ b/Sources/StreamChat/Generated/OpenAPI/models/MutedChannelPayload.swift @@ -5,6 +5,7 @@ import Foundation final class MutedChannelPayload: Sendable, Codable, JSONEncodable { + /// Represents channel in chat let channel: ChannelDetailPayload? /// Date/time of creation let createdAt: Date @@ -12,6 +13,7 @@ final class MutedChannelPayload: Sendable, Codable, JSONEncodable { let expires: Date? /// Date/time of the last update let updatedAt: Date + /// User response object let user: UserPayload? init( diff --git a/Sources/StreamChat/Generated/OpenAPI/models/PollPayload.swift b/Sources/StreamChat/Generated/OpenAPI/models/PollPayload.swift index 728658ebe73..b154e40292b 100644 --- a/Sources/StreamChat/Generated/OpenAPI/models/PollPayload.swift +++ b/Sources/StreamChat/Generated/OpenAPI/models/PollPayload.swift @@ -9,6 +9,7 @@ final class PollPayload: Sendable, Codable, JSONEncodable { let allowUserSuggestedOptions: Bool let answersCount: Int let createdAt: Date + /// User response object let createdBy: UserPayload? let createdById: String let custom: [String: RawJSON]? diff --git a/Sources/StreamChat/Generated/OpenAPI/models/PollVotePayload.swift b/Sources/StreamChat/Generated/OpenAPI/models/PollVotePayload.swift index 890d2030a74..c8d3298a88c 100644 --- a/Sources/StreamChat/Generated/OpenAPI/models/PollVotePayload.swift +++ b/Sources/StreamChat/Generated/OpenAPI/models/PollVotePayload.swift @@ -12,6 +12,7 @@ final class PollVotePayload: Sendable, Codable, JSONEncodable { let optionId: String? let pollId: String let updatedAt: Date + /// User response object let user: UserPayload? let userId: String? diff --git a/Sources/StreamChat/Generated/OpenAPI/models/PushPreferenceInput.swift b/Sources/StreamChat/Generated/OpenAPI/models/PushPreferenceInput.swift index 8eeaff84ea6..f640319be33 100644 --- a/Sources/StreamChat/Generated/OpenAPI/models/PushPreferenceInput.swift +++ b/Sources/StreamChat/Generated/OpenAPI/models/PushPreferenceInput.swift @@ -4,31 +4,26 @@ import Foundation -final class PushPreferenceInput: Sendable, Codable, JSONEncodable { - enum PushPreferenceInputChatLevel: String, Sendable, Codable, CaseIterable { - case `default` - case all - case allMentions = "all_mentions" - case directMentions = "direct_mentions" - case mentions - case none - case unknown = "_unknown" +public struct PushPreferenceLevel: RawRepresentable, Codable, Hashable, Sendable { + public let rawValue: String - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let decodedValue = try? container.decode(String.self), - let value = Self(rawValue: decodedValue) { - self = value - } else { - self = .unknown - } - } + public init(rawValue: String) { + self.rawValue = rawValue } - + + public static let `default` = Self(rawValue: "default") + public static let all = Self(rawValue: "all") + public static let allMentions = Self(rawValue: "all_mentions") + public static let directMentions = Self(rawValue: "direct_mentions") + public static let mentions = Self(rawValue: "mentions") + public static let none = Self(rawValue: "none") +} + +final class PushPreferenceInput: Sendable, Codable, JSONEncodable { /// Set the push preferences for a specific channel. If empty it sets the default for the user let channelCid: String? /// Set the level of chat push notifications for the user. Note: "mentions" is deprecated in favor of "direct_mentions". One of: all, mentions, direct_mentions, all_mentions, none, default - let chatLevel: PushPreferenceInputChatLevel? + let chatLevel: PushPreferenceLevel? /// Disable push notifications till a certain time let disabledUntil: Date? /// Remove the disabled until time. (IE stop snoozing notifications) @@ -38,7 +33,7 @@ final class PushPreferenceInput: Sendable, Codable, JSONEncodable { init( channelCid: String? = nil, - chatLevel: PushPreferenceInputChatLevel? = nil, + chatLevel: PushPreferenceLevel? = nil, disabledUntil: Date? = nil, removeDisable: Bool? = nil, userId: String? = nil diff --git a/Sources/StreamChat/Generated/OpenAPI/models/UpdateLiveLocationRequest.swift b/Sources/StreamChat/Generated/OpenAPI/models/UpdateLiveLocationRequest.swift index 48a1ee53bf9..0a34d346bd4 100644 --- a/Sources/StreamChat/Generated/OpenAPI/models/UpdateLiveLocationRequest.swift +++ b/Sources/StreamChat/Generated/OpenAPI/models/UpdateLiveLocationRequest.swift @@ -8,16 +8,16 @@ final class UpdateLiveLocationRequest: Sendable, Codable, JSONEncodable { /// Time when the live location expires let endAt: Date? /// Latitude coordinate - let latitude: Float? + let latitude: Double? /// Longitude coordinate - let longitude: Float? + let longitude: Double? /// Live location ID let messageId: String init( endAt: Date? = nil, - latitude: Float? = nil, - longitude: Float? = nil, + latitude: Double? = nil, + longitude: Double? = nil, messageId: String ) { self.endAt = endAt diff --git a/Sources/StreamChat/Models/Channel.swift b/Sources/StreamChat/Models/Channel.swift index 7bdd99927f0..2f9ee104a92 100644 --- a/Sources/StreamChat/Models/Channel.swift +++ b/Sources/StreamChat/Models/Channel.swift @@ -541,96 +541,6 @@ public struct ChannelUnreadCount: Decodable, Equatable, Sendable { } } -/// An action that can be performed in a channel. -public struct ChannelCapability: RawRepresentable, ExpressibleByStringLiteral, Hashable, Sendable { - public var rawValue: String - - public init?(rawValue: String) { - self.rawValue = rawValue - } - - public init(stringLiteral value: String) { - rawValue = value - } - - /// Ability to ban channel members. - public static let banChannelMembers: Self = "ban-channel-members" - /// Ability to receive connect events. - public static let connectEvents: Self = "connect-events" - /// Ability to delete any message from the channel. - public static let deleteAnyMessage: Self = "delete-any-message" - /// Ability to delete the channel. - public static let deleteChannel: Self = "delete-channel" - /// Ability to delete own messages from the channel. - public static let deleteOwnMessage: Self = "delete-own-message" - /// Ability to flag a message. - public static let flagMessage: Self = "flag-message" - /// Ability to freeze or unfreeze the channel. - public static let freezeChannel: Self = "freeze-channel" - /// Ability to leave the channel (remove own membership). - public static let leaveChannel: Self = "leave-channel" - /// Ability to join channel (add own membership). - public static let joinChannel: Self = "join-channel" - /// Ability to mute the channel. - public static let muteChannel: Self = "mute-channel" - /// Ability to pin a message. - public static let pinMessage: Self = "pin-message" - /// Ability to quote a message. - public static let quoteMessage: Self = "quote-message" - /// Ability to receive read events. - public static let readEvents: Self = "read-events" - /// Ability to use message search. - public static let searchMessages: Self = "search-messages" - /// Ability to send custom events. - public static let sendCustomEvents: Self = "send-custom-events" - /// Ability to attach links to messages. - public static let sendLinks: Self = "send-links" - /// Ability to send a message. - public static let sendMessage: Self = "send-message" - /// Ability to send reactions. - public static let sendReaction: Self = "send-reaction" - /// Ability to thread reply to a message. - public static let sendReply: Self = "send-reply" - /// Ability to enable or disable slow mode. - public static let setChannelCooldown: Self = "set-channel-cooldown" - /// Ability to send and receive typing events. - public static let sendTypingEvents: Self = "send-typing-events" - /// Ability to update any message in the channel. - public static let updateAnyMessage: Self = "update-any-message" - /// Ability to update channel data. - public static let updateChannel: Self = "update-channel" - /// Ability to update channel members. - public static let updateChannelMembers: Self = "update-channel-members" - /// Ability to update own messages in the channel. - public static let updateOwnMessage: Self = "update-own-message" - /// Ability to upload message attachments. - public static let uploadFile: Self = "upload-file" - /// Ability to send and receive typing events. - public static let typingEvents: Self = "typing-events" - /// Indicates that channel slow mode is active. - public static let slowMode: Self = "slow-mode" - /// Ability to skip the slow mode when it's active. - public static let skipSlowMode: Self = "skip-slow-mode" - /// Ability to join a call. - public static let joinCall: Self = "join-call" - /// Ability to create a call. - public static let createCall: Self = "create-call" - /// Ability to send a poll. - public static let sendPoll: Self = "send-poll" - /// Ability to cast a poll vote. - public static let castPollVote: Self = "cast-poll-vote" - /// Ability to share location. - public static let shareLocation: Self = "share-location" - /// Ability to mention the whole channel (`@channel`). - public static let notifyChannel: Self = "notify-channel" - /// Ability to mention a user group (`@group`). - public static let notifyGroup: Self = "notify-group" - /// Ability to mention the online members of the channel (`@here`). - public static let notifyHere: Self = "notify-here" - /// Ability to mention members that have a given role (`@role`). - public static let notifyRole: Self = "notify-role" -} - public extension ChatChannel { /// Can the current user ban members from this channel. var canBanChannelMembers: Bool { @@ -763,11 +673,13 @@ public extension ChatChannel { } /// Can the current user join a call in this channel. + @available(*, deprecated, message: "Calling capabilities are no longer part of channel capabilities.") var canJoinCall: Bool { ownCapabilities.contains(.joinCall) } /// Can the current user create a call in this channel. + @available(*, deprecated, message: "Calling capabilities are no longer part of channel capabilities.") var canCreateCall: Bool { ownCapabilities.contains(.createCall) } diff --git a/Sources/StreamChat/Models/ChannelCapability+Extensions.swift b/Sources/StreamChat/Models/ChannelCapability+Extensions.swift new file mode 100644 index 00000000000..bedc18b892f --- /dev/null +++ b/Sources/StreamChat/Models/ChannelCapability+Extensions.swift @@ -0,0 +1,21 @@ +// +// Copyright © 2026 Stream.io Inc. All rights reserved. +// + +import Foundation + +extension ChannelCapability: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self.init(rawValue: value) + } +} + +public extension ChannelCapability { + /// Ability to join a call. + @available(*, deprecated, message: "Calling capabilities are no longer part of channel capabilities.") + static let joinCall: Self = "join-call" + + /// Ability to create a call. + @available(*, deprecated, message: "Calling capabilities are no longer part of channel capabilities.") + static let createCall: Self = "create-call" +} diff --git a/Sources/StreamChat/Models/Payload+asModel/ChannelPayload+asModel.swift b/Sources/StreamChat/Models/Payload+asModel/ChannelPayload+asModel.swift index 119bbfa002c..66f39eb9ab4 100644 --- a/Sources/StreamChat/Models/Payload+asModel/ChannelPayload+asModel.swift +++ b/Sources/StreamChat/Models/Payload+asModel/ChannelPayload+asModel.swift @@ -43,7 +43,7 @@ extension ChannelPayload { createdBy: channelPayload.createdBy?.asModel(), config: channelPayload.config, filterTags: Set(channelPayload.filterTags ?? []), - ownCapabilities: Set(channelPayload.ownCapabilities?.compactMap { ChannelCapability(rawValue: $0.rawValue) } ?? []), + ownCapabilities: Set(channelPayload.ownCapabilities ?? []), isFrozen: channelPayload.frozen, isDisabled: channelPayload.disabled, isBlocked: channelPayload.blocked ?? false, diff --git a/Sources/StreamChat/Models/PushPreferences/PushPreferenceLevel.swift b/Sources/StreamChat/Models/PushPreferences/PushPreferenceLevel.swift index 23693f6cd9d..d0e648e9b83 100644 --- a/Sources/StreamChat/Models/PushPreferences/PushPreferenceLevel.swift +++ b/Sources/StreamChat/Models/PushPreferences/PushPreferenceLevel.swift @@ -4,42 +4,8 @@ import Foundation -/// The scope level of the push notifications. -public struct PushPreferenceLevel: RawRepresentable, Hashable, ExpressibleByStringLiteral, Sendable { - public let rawValue: String - - public init(rawValue: String) { - self.rawValue = rawValue - } - +extension PushPreferenceLevel: ExpressibleByStringLiteral { public init(stringLiteral value: StringLiteralType) { - rawValue = value - } - - /// No push notifications will be delivered. - public static let none: PushPreferenceLevel = "none" - /// Push notifications will only be delivered for mentions. - @available(*, deprecated, renamed: "directMentions") - public static let mentions: PushPreferenceLevel = "mentions" - /// Push notifications will be delivered for any kind of mention, including - /// explicit @mentions, @channel, @here, group, and role mentions. - public static let allMentions: PushPreferenceLevel = "all_mentions" - /// Push notifications will only be delivered for explicit @mentions by username - /// and thread replies. - public static let directMentions: PushPreferenceLevel = "direct_mentions" - /// All push notifications will be delivered. - public static let all: PushPreferenceLevel = "all" -} - -extension PushPreferenceLevel: Codable { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let rawValue = try? container.decode(String.self) - self.init(rawValue: rawValue ?? Self.all.rawValue) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(rawValue) + self.init(rawValue: value) } } diff --git a/Sources/StreamChat/Repositories/PollsRepository.swift b/Sources/StreamChat/Repositories/PollsRepository.swift index 8f5d3a19aab..2b5415c9361 100644 --- a/Sources/StreamChat/Repositories/PollsRepository.swift +++ b/Sources/StreamChat/Repositories/PollsRepository.swift @@ -20,7 +20,7 @@ class PollsRepository: @unchecked Sendable { description: String?, enforceUniqueVote: Bool?, maxVotesAllowed: Int?, - votingVisibility: String?, + votingVisibility: VotingVisibility?, options: [PollOption]?, custom: [String: RawJSON]?, completion: @escaping @Sendable (Result) -> Void @@ -34,7 +34,7 @@ class PollsRepository: @unchecked Sendable { maxVotesAllowed: maxVotesAllowed, name: name, options: options?.compactMap { PollOptionRequestBody(custom: $0.extraData, text: $0.text) }, - votingVisibility: votingVisibility.flatMap { .init(rawValue: $0) } + votingVisibility: votingVisibility ) apiClient.request( endpoint: .createPoll(createPollRequest: request) diff --git a/Sources/StreamChat/Workers/CurrentUserUpdater.swift b/Sources/StreamChat/Workers/CurrentUserUpdater.swift index 228badd3e62..1e356911d3c 100644 --- a/Sources/StreamChat/Workers/CurrentUserUpdater.swift +++ b/Sources/StreamChat/Workers/CurrentUserUpdater.swift @@ -86,7 +86,7 @@ class CurrentUserUpdater: Worker, @unchecked Sendable { endpoint: .createDevice( createDeviceRequest: CreateDeviceRequest( id: deviceId, - pushProvider: .init(rawValue: pushProvider.rawValue) ?? .unknown, + pushProvider: .init(rawValue: pushProvider.rawValue), pushProviderName: providerName ) ), diff --git a/Sources/StreamChat/Workers/MessageUpdater.swift b/Sources/StreamChat/Workers/MessageUpdater.swift index 3d6f2828385..3fb24435607 100644 --- a/Sources/StreamChat/Workers/MessageUpdater.swift +++ b/Sources/StreamChat/Workers/MessageUpdater.swift @@ -366,8 +366,8 @@ class MessageUpdater: Worker, @unchecked Sendable { } let request = UpdateLiveLocationRequest( - latitude: Float(locationInfo.latitude), - longitude: Float(locationInfo.longitude), + latitude: locationInfo.latitude, + longitude: locationInfo.longitude, messageId: messageId ) diff --git a/Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift b/Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift index 32d9484dcd2..e1f9887849c 100644 --- a/Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift +++ b/Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift @@ -349,6 +349,23 @@ final class ChannelDTO_Tests: XCTestCase { XCTAssertEqual(loadedChannel.truncatedBy?.id, truncatedBy.id) } + func test_saveChannel_keepsUnrecognizedOwnCapability() throws { + let channelId: ChannelId = .unique + let payload = ChannelDetailPayload.dummy( + cid: channelId, + ownCapabilities: [ChannelCapability.sendMessage.rawValue, "future-capability"] + ) + + try database.writeSynchronously { session in + try session.saveChannel(payload: payload, query: nil, cache: nil) + } + + let loadedChannel = try database.readSynchronously { session in + try XCTUnwrap(session.channel(cid: channelId)?.asModel()) + } + XCTAssertEqual(loadedChannel.ownCapabilities, [.sendMessage, ChannelCapability(rawValue: "future-capability")]) + } + func test_saveChannel_storesAutoTranslation() throws { let channelId: ChannelId = .unique let payload = ChannelDetailPayload.dummy( diff --git a/Tests/StreamChatTests/Models/ChatChannel_Tests.swift b/Tests/StreamChatTests/Models/ChatChannel_Tests.swift index 555d9d07cfc..1957907a92e 100644 --- a/Tests/StreamChatTests/Models/ChatChannel_Tests.swift +++ b/Tests/StreamChatTests/Models/ChatChannel_Tests.swift @@ -43,12 +43,12 @@ final class ChatChannel_Tests: XCTestCase { func test_ownCapabilities() { let channel: ChatChannel = .mock( cid: .unique, - ownCapabilities: [.banChannelMembers, .createCall, .deleteAnyMessage], + ownCapabilities: [.banChannelMembers, .createAttachment, .deleteAnyMessage], unreadCount: .noUnread ) XCTAssertTrue(channel.ownCapabilities.contains(.banChannelMembers)) - XCTAssertTrue(channel.ownCapabilities.contains(.createCall)) + XCTAssertTrue(channel.ownCapabilities.contains(.createAttachment)) XCTAssertTrue(channel.ownCapabilities.contains(.deleteAnyMessage)) XCTAssertFalse(channel.ownCapabilities.contains(.freezeChannel)) } @@ -261,22 +261,6 @@ final class ChatChannel_Tests: XCTestCase { XCTAssertEqual(channelWithoutCapability.canUploadFile, false) } - func test_canJoinCall() throws { - let channel = setupChannel(withCapabilities: [.joinCall]) - XCTAssertEqual(channel.canJoinCall, true) - - let channelWithoutCapability = setupChannel(withCapabilities: []) - XCTAssertEqual(channelWithoutCapability.canJoinCall, false) - } - - func test_canCreateCall() throws { - let channel = setupChannel(withCapabilities: [.createCall]) - XCTAssertEqual(channel.canCreateCall, true) - - let channelWithoutCapability = setupChannel(withCapabilities: []) - XCTAssertEqual(channelWithoutCapability.canCreateCall, false) - } - func test_isSlowMode() throws { let channel = setupChannel(withCapabilities: [.slowMode]) XCTAssertEqual(channel.isSlowMode, true) diff --git a/Tests/StreamChatTests/Workers/CurrentUserUpdater_Tests.swift b/Tests/StreamChatTests/Workers/CurrentUserUpdater_Tests.swift index bc6fc03eff7..ff7eba15f17 100644 --- a/Tests/StreamChatTests/Workers/CurrentUserUpdater_Tests.swift +++ b/Tests/StreamChatTests/Workers/CurrentUserUpdater_Tests.swift @@ -336,7 +336,7 @@ final class CurrentUserUpdater_Tests: XCTestCase { let expectedEndpoint = Endpoint.createDevice( createDeviceRequest: CreateDeviceRequest( id: deviceId, - pushProvider: .init(rawValue: pushProvider.rawValue) ?? .unknown, + pushProvider: .init(rawValue: pushProvider.rawValue), pushProviderName: providerName ) )