Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
4 changes: 2 additions & 2 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ file_name_no_space:

cyclomatic_complexity:
ignores_case_statements: true
warning: 30
error: 35
warning: 35
error: 40
Comment thread
laevandus marked this conversation as resolved.
Outdated

custom_rules:
regular_constraints_forbidden:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add `MemberSearch` for debounced channel member search [#4213](https://github.com/GetStream/stream-chat-swift/pull/4213)
- Add `ChatChannel.truncatedBy`, `ChatChannel.isAutoTranslationEnabled` and `ChatChannel.autoTranslationLanguages` [#4197](https://github.com/GetStream/stream-chat-swift/pull/4197)
- Add `ChatChannelMember.memberStatus` and `ChatChannelMember.memberDeletedAt` [#4197](https://github.com/GetStream/stream-chat-swift/pull/4197)
- Add `MessageModerationDetails.blocklistsMatched`, `MessageModerationDetails.textHarms`, `MessageModerationDetails.imageHarms`, `MessageModerationDetails.semanticFilterMatched` and `MessageModerationDetails.platformCircumvented`
### 🐞 Fixed
- Fix unread count not clearing immediately when marking a channel as read [#4214](https://github.com/GetStream/stream-chat-swift/pull/4214)
- Fix mention suggestions not showing members in channels with 100+ members in the `MentionSuggestionsProvider` [#4213](https://github.com/GetStream/stream-chat-swift/pull/4213)
Expand Down
74 changes: 66 additions & 8 deletions Scripts/openapi_generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ allowed_endpoints=(
blockUsers
castPollVote
createDevice
createDraft
createPoll
createPollOption
createUserGroup
Expand Down Expand Up @@ -44,13 +45,16 @@ allowed_endpoints=(
removeUserGroupMembers
searchRoles
searchUserGroups
sendMessage
showChannel
stopWatchingChannel
unblockUsers
unmuteChannel
unreadCounts
updateLiveLocation
updateMemberPartial
updateMessage
updateMessagePartial
updatePollPartial
updatePushNotificationPreferences
updateUserGroup
Expand All @@ -63,22 +67,28 @@ allowed_models=(
Action
AddUserGroupMembersRequest
AppResponseFields
Attachment
BlockedUserResponse
BlockUsersRequest
BlockUsersResponse
CastPollVoteRequest
ChannelMemberPartialResponse
ChannelMemberRequest
ChannelMemberResponse
ChannelMute
ChannelOwnCapability
ChannelResponse
CreateDeviceRequest
CreateDraftRequest
CreateDraftResponse
CreatePollOptionRequest
CreatePollRequest
CreateUserGroupRequest
DeleteChannelResponse
DeliveredMessagePayload
DeviceResponse
DraftPayloadResponse
DraftResponse
Field
FileUploadConfig
FileUploadResponse
Expand All @@ -96,6 +106,8 @@ allowed_models=(
ListUserGroupsResponse
MarkDeliveredRequest
MembersResponse
MessageRequest
ModerationV2Response
MuteChannelRequest
MuteChannelResponse
PollOptionInput
Expand All @@ -111,10 +123,16 @@ allowed_models=(
QueryMembersPayload
QueryPollVotesRequest
QueryReactionsRequest
ReactionGroupResponse
ReactionResponse
ReminderResponseData
RemoveUserGroupMembersRequest
Role
SearchResultMessage
SearchRolesResponse
SendMessageRequest
SendMessageResponse
SharedLocation
SharedLocationResponseData
SharedLocationsResponse
SortParamRequest
Expand All @@ -127,6 +145,10 @@ allowed_models=(
UpdateLiveLocationRequest
UpdateMemberPartialRequest
UpdateMemberPartialResponse
UpdateMessagePartialRequest
UpdateMessagePartialResponse
UpdateMessageRequest
UpdateMessageResponse
UpdatePollPartialRequest
UpdateUserGroupRequest
UploadChannelFileResponse
Expand Down Expand Up @@ -377,6 +399,7 @@ rename_generated_type CreateUserGroupResponse UserGroupResponse
rename_generated_type RemoveUserGroupMembersResponse UserGroupResponse
rename_generated_type UpdateUserGroupResponse UserGroupResponse
rename_generated_type SearchUserGroupsResponse ListUserGroupsResponse
rename_generated SharedLocation NewLocationRequestPayload

@laevandus laevandus Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming conflict with our public API layer and what generator uses.
SharedLocation type name was taken by us in the public API layer and it does not equal to what generator generates. Therefore, we keep using the other NewLocationRequestPayload name.

rename_generated SharedLocationResponseData SharedLocation
rename_generated_type SharedLocationResponse SharedLocation
rename_generated MarkDeliveredRequest ChannelDeliveredRequestPayload
Expand All @@ -400,6 +423,16 @@ rename_generated ChannelMemberResponse MemberPayload
rename_generated ChannelMute MutedChannelPayload
rename_generated ChannelResponse ChannelDetailPayload
rename_generated MuteChannelResponse MutedChannelPayloadResponse
rename_generated Attachment MessageAttachmentPayload
rename_generated ChannelMemberPartialResponse MemberInfoPayload
rename_generated DraftPayloadResponse DraftMessagePayload
rename_generated DraftResponse DraftPayload
rename_generated ModerationV2Response MessageModerationDetailsPayload
rename_generated ReactionGroupResponse MessageReactionGroupPayload
rename_generated ReminderResponseData ReminderPayload
rename_generated SearchResultMessage MessagePayload
rename_generated SendMessageResponse SendMessageResponsePayload
rename_generated_type MessageResponse MessagePayload

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping the old payload names to reduce the PR size just because of type renames.

Note:
Our hand-crafted MessagePayload includes optional channel property to make it compatible with message search responses. This is why generated SearchResultMessage (MessageResponse fields + optional channel field) gets reused as MessagePayload. We don't really want to have multiple similar generated types which just differ from one property (SDK size starts to go up). Not very nice, but good for SDK size.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, how does that exactly work? Both being renamed to the same payload?
Also, it's a bit risky to use the search payload for the message payload.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am gonna try using 2 types: MessageResponse and SearchResultMessage (used by message search) (the most correct solution)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, opened up a nice cleanup in saveMessage flows as well

Comment thread
laevandus marked this conversation as resolved.
Outdated

rename_generated_type HideChannelResponse EmptyResponse
rename_generated_type MarkDeliveredResponse EmptyResponse
Expand Down Expand Up @@ -479,12 +512,45 @@ remove_property DeleteChannelResponse duration
remove_property MutedChannelPayloadResponse channelMutes
remove_property MutedChannelPayloadResponse duration
remove_property MutedChannelPayloadResponse ownUser
remove_property CreateDraftResponse duration
# include_channel_context flag is never set and this value is always nil
remove_property SendMessageResponsePayload channelContext
Comment thread
laevandus marked this conversation as resolved.
Outdated
remove_property SendMessageResponsePayload duration
remove_property UpdateMessagePartialResponse duration
remove_property UpdateMessageResponse duration

# Server-side only: client-side requests cannot set these fields
remove_property SendMessageResponsePayload pendingMessageMetadata
remove_property UpdateMessagePartialResponse pendingMessageMetadata
remove_property UpdateMessageResponse pendingMessageMetadata
Comment thread
laevandus marked this conversation as resolved.
Comment on lines +520 to +523

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CHA-4779


# TODO: reaction group reactors need CoreData and public API design first
remove_property MessageReactionGroupPayload latestReactionsBy
Comment on lines +525 to +526

@laevandus laevandus Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for later (separate PR). Captured in IOS-1972


retype_property ChannelDetailPayload cid String ChannelId
retype_property ChannelDetailPayload config ChannelConfigWithInfo ChannelConfig
# Will be changed on the generation side later
require_property ChannelDetailPayload config

# TODO: v1 and v2 require this field; removing compatibility requires JSON fixture normalization.
optionalize_property DraftPayload channelCid
# TODO: v1 and v2 require this field; removing compatibility requires JSON fixture normalization.
optionalize_property MessagePayload cid
# TODO: v1 and v2 require this field; removing compatibility requires JSON fixture normalization.
optionalize_property MessagePayload deletedReplyCount
# TODO: v1 and v2 require this field; removing compatibility requires JSON fixture normalization.
optionalize_property MessagePayload mentionedChannel
# TODO: v1 and v2 require this field; removing compatibility requires JSON fixture normalization.
optionalize_property MessagePayload mentionedHere
# TODO: v1 and v2 require this field; removing compatibility requires JSON fixture normalization.
optionalize_property MessagePayload pinned
# TODO: Legacy v1 payloads may contain null; keep optional until legacy compatibility is removed.
optionalize_property MessagePayload reactionCounts
# TODO: v1 and v2 require this field; removing compatibility requires JSON fixture normalization.
optionalize_property MessagePayload restrictedVisibility
# TODO: v1 and v2 require this field; removing compatibility requires JSON fixture normalization.
optionalize_property MessagePayload shadowed

remove_nested_enum() {
local file="$OUTPUT_DIR_CHAT/models/$1.swift"
awk -v e="$2" '
Expand Down Expand Up @@ -590,12 +656,8 @@ inject_v1_endpoint_paths() {
case channelEvent(String)
case pinnedMessages(String)

case sendMessage(ChannelId)
case message(MessageId)
case editMessage(MessageId)
case deleteMessage(MessageId)
case pinMessage(MessageId)
case unpinMessage(MessageId)
case replies(MessageId)
case addReaction(MessageId)
case deleteReaction(MessageId, MessageReactionType)
Expand Down Expand Up @@ -649,12 +711,8 @@ EOF
case let .channelEvent(channelId): return "channels/\(channelId)/event"
case let .pinnedMessages(channelId): return "channels/\(channelId)/pinned_messages"

case let .sendMessage(channelId): return "channels/\(channelId.apiPath)/message"
case let .message(messageId): return "messages/\(messageId)"
case let .editMessage(messageId): return "messages/\(messageId)"
case let .deleteMessage(messageId): return "messages/\(messageId)"
case let .pinMessage(messageId): return "messages/\(messageId)"
case let .unpinMessage(messageId): return "messages/\(messageId)"
case let .replies(messageId): return "messages/\(messageId)/replies"
case let .addReaction(messageId): return "messages/\(messageId)/reaction"
case let .deleteReaction(messageId, reaction): return "messages/\(messageId)/reaction/\(reaction.rawValue)"
Expand Down
5 changes: 5 additions & 0 deletions Sources/StreamChat/.openapi.sourcery.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
sources:
- ./Generated/OpenAPI/models/ChannelDetailPayload.swift
- ./Generated/OpenAPI/models/DraftMessagePayload.swift
- ./Generated/OpenAPI/models/MemberInfoPayload.swift
- ./Generated/OpenAPI/models/MemberPayload.swift
- ./Generated/OpenAPI/models/MessageAttachmentPayload.swift
- ./Generated/OpenAPI/models/MessagePayload.swift
- ./Generated/OpenAPI/models/MessageReactionPayload.swift
- ./Generated/OpenAPI/models/UserPayload.swift
templates:
Expand All @@ -11,4 +15,5 @@ args:
# v1 endpoints return the user's custom data flattened next to a wider set of keys than v2
# declares, so those have to be excluded too when rebuilding the extra data.
v1CodingKeys:
MessagePayload: MessagePayloadsCodingKeys
UserPayload: UserPayloadsCodingKeys
21 changes: 0 additions & 21 deletions Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,27 +101,6 @@ extension Endpoint {
)
}

static func sendMessage(
cid: ChannelId,
messagePayload: MessageRequestBody,
skipPush: Bool,
skipEnrichUrl: Bool
)
-> Endpoint<MessagePayload.Boxed> {
let body: [String: AnyEncodable] = [
"message": AnyEncodable(messagePayload),
"skip_push": AnyEncodable(skipPush),
"skip_enrich_url": AnyEncodable(skipEnrichUrl)
]
return .init(
path: .sendMessage(cid),
method: .post,
queryItems: nil,
requiresConnectionId: false,
body: body
)
}

static func addMembers(
cid: ChannelId,
members: [MemberInfoRequest],
Expand Down
16 changes: 0 additions & 16 deletions Sources/StreamChat/APIClient/Endpoints/DraftEndpoints.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,6 @@ extension Endpoint {
)
}

static func updateDraftMessage(
channelId: ChannelId,
requestBody: DraftMessageRequestBody
) -> Endpoint<DraftPayloadResponse> {
let body: [String: AnyEncodable] = [
"message": AnyEncodable(requestBody)
]
return .init(
path: .draftMessage(channelId),
method: .post,
queryItems: nil,
requiresConnectionId: false,
body: body
)
}

static func getDraftMessage(
channelId: ChannelId,
threadId: MessageId?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ extension EndpointPath {
var shouldBeQueuedOffline: Bool {
switch self {
case .addReaction,
.createDraft,
.deleteMessage,
.deleteReaction,
.draftMessage,
.editMessage,
.pinMessage,
.sendMessage,
.unpinMessage:
.updateMessage,
.updateMessagePartial:
Comment thread
laevandus marked this conversation as resolved.
return true
case .addUserGroupMembers,
.banMember,
Expand Down
79 changes: 0 additions & 79 deletions Sources/StreamChat/APIClient/Endpoints/MessageEndpoints.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,43 +29,6 @@ extension Endpoint {
)
}

static func editMessage(payload: MessageRequestBody, skipEnrichUrl: Bool, skipPush: Bool)
-> Endpoint<EmptyResponse> {
.init(
path: .editMessage(payload.id),
method: .post,
queryItems: nil,
requiresConnectionId: false,
body: [
"message": AnyEncodable(payload),
"skip_enrich_url": AnyEncodable(skipEnrichUrl),
"skip_push": AnyEncodable(skipPush)
]
)
}

static func pinMessage(messageId: MessageId, request: MessagePartialUpdateRequest)
-> Endpoint<EmptyResponse> {
.init(
path: .pinMessage(messageId),
method: .put,
queryItems: nil,
requiresConnectionId: false,
body: request
)
}

static func partialUpdateMessage(messageId: MessageId, request: MessagePartialUpdateRequest)
-> Endpoint<MessagePayload.Boxed> {
.init(
path: .editMessage(messageId),
method: .put,
queryItems: nil,
requiresConnectionId: false,
body: request
)
}

static func loadReplies(messageId: MessageId, pagination: MessagesPagination)
-> Endpoint<MessageRepliesPayload> {
.init(
Expand Down Expand Up @@ -109,45 +72,3 @@ extension Endpoint {
)
}
}

// MARK: - Helper data structures

struct MessagePartialUpdateRequest: Encodable {
var set: SetProperties?
var unset: [String]?
var skipEnrichUrl: Bool?
var userId: String?
var user: UserRequestBody?

/// The available message properties that can be updated.
struct SetProperties: Encodable {
var pinned: Bool?
var text: String?
var extraData: [String: RawJSON]?
var attachments: [MessageAttachmentPayload]?

enum CodingKeys: String, CodingKey {
case text
case pinned
case extraData
case attachments
}

func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(text, forKey: .text)
try container.encodeIfPresent(pinned, forKey: .pinned)
try container.encodeIfPresent(attachments, forKey: .attachments)
try extraData?.encode(to: encoder)
}
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: MessagePayloadsCodingKeys.self)
try container.encodeIfPresent(skipEnrichUrl, forKey: .skipEnrichUrl)
try container.encodeIfPresent(userId, forKey: .userId)
try container.encodeIfPresent(user, forKey: .user)
try container.encodeIfPresent(set, forKey: .set)
try container.encodeIfPresent(unset, forKey: .unset)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// Copyright © 2026 Stream.io Inc. All rights reserved.
//

import Foundation

// Generated properties are slightly different from the previously hand-written ones.
extension DraftMessagePayload {
var command: String? { custom[MessagePayloadsCodingKeys.command.rawValue]?.stringValue }
var args: String? { custom[MessagePayloadsCodingKeys.args.rawValue]?.stringValue }
}
Loading
Loading