Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e5d590f
open ChannelInfoViewModel control proprties
KhaledElsherbeny Oct 1, 2025
55b5a0b
fix ios26 navigationBarTrailing style
KhaledElsherbeny Oct 2, 2025
2e568c5
Merge branch 'GetStream:develop' into develop
KhaledElsherbeny Oct 2, 2025
562d874
open mute channel control property for ChannelInfoViewModel
KhaledElsherbeny Oct 16, 2025
b00daff
open channel info leave actions title
KhaledElsherbeny Oct 16, 2025
19e65dc
cleanup
KhaledElsherbeny Oct 17, 2025
82cd117
Merge branch 'GetStream:develop' into develop
KhaledElsherbeny Oct 17, 2025
0360af4
Fix:Owner user not marked as owner in channel/group details screen
KhaledKamalA Oct 18, 2025
9781f20
Fix:Some items in chat screens need UI enhancement
KhaledKamalA Oct 20, 2025
2f171a3
Fix:Remove channel / group options icon
KhaledKamalA Oct 20, 2025
b2be7fa
Fix: Number of users in channel not updated after adding a new members
KhaledKamalA Oct 20, 2025
49ae19a
Fix: checking owner condition
KhaledKamalA Oct 20, 2025
2e0846d
Merge branch 'Bug-Fixes/Some-items-in-chat-screens-need-UI-enhancemen…
KhaledKamalA Oct 21, 2025
0ade1eb
Merge branch 'Bug-Fixes/Remove-channel-or-group-options-icon' into de…
KhaledKamalA Oct 21, 2025
a406060
Merge branch 'Bug-Fixes/channels-number-of-users-in-channel-not-updat…
KhaledKamalA Oct 21, 2025
53227c7
Refact: make removeUserAction optional and open for override
KhaledKamalA Oct 21, 2025
0fe4ac9
Update action DirectMessage icon from message.circle.fill to message
KhaledKamalA Oct 22, 2025
7c91fb8
hide trailing swipe when actions is empty
KhaledKamalA Oct 22, 2025
389d7dd
Merge branch 'change-action-message-icon' into develop
KhaledKamalA Oct 22, 2025
8112754
open some ChatChannelInfoViewModel methods for inheritance
KhaledKamalA Oct 23, 2025
f11c47d
Merge branch 'develop' into open-ChatChannelInfoViewModel-remove-meth…
KhaledKamalA Oct 23, 2025
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
2 changes: 1 addition & 1 deletion DemoAppSwiftUI/CreateGroupView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ struct CreateGroupView: View, KeyboardReadable {
}
}
.listStyle(.plain)
}
}.padding(.top)
.toolbarThemed(content: {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,15 @@ public struct ChatInfoOptionsView<Factory: ViewFactory>: View {

Divider()

ChannelInfoItemView(
icon: images.muted,
title: viewModel.mutedText,
verticalPadding: 12
) {
Toggle(isOn: $viewModel.muted) {
EmptyView()
if viewModel.shouldShowMuteChannelButton {
ChannelInfoItemView(
icon: images.muted,
title: viewModel.mutedText,
verticalPadding: 12
) {
Toggle(isOn: $viewModel.muted) {
EmptyView()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public struct ChatChannelInfoView<Factory: ViewFactory>: View, KeyboardReadable
ChatInfoParticipantsView(
factory: factory,
participants: viewModel.displayedParticipants,
channel: viewModel.channel,
onItemAppear: viewModel.onParticipantAppear(_:),
selectedParticipant: $viewModel.selectedParticipant
)
Expand Down Expand Up @@ -101,7 +102,7 @@ public struct ChatChannelInfoView<Factory: ViewFactory>: View, KeyboardReadable
)
}
}
}
}.padding(.top)
}
.overlay(
popupShown ?
Expand Down Expand Up @@ -158,7 +159,8 @@ struct ChatChannelInfoViewHeaderViewModifier: ViewModifier {
@Injected(\.colors) private var colors
@Injected(\.fonts) private var fonts

let viewModel: ChatChannelInfoViewModel
@ObservedObject var viewModel: ChatChannelInfoViewModel


func body(content: Content) -> some View {
if #available(iOS 26.0, *) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ open class ChatChannelInfoViewModel: ObservableObject, ChatChannelControllerDele
channel.ownCapabilities.contains(.leaveChannel)
}
}

open var shouldShowMuteChannelButton: Bool {
channel.ownCapabilities.contains(.muteChannel)
}

open var canRenameChannel: Bool {
channel.ownCapabilities.contains(.updateChannel)
Expand Down Expand Up @@ -87,15 +91,15 @@ open class ChatChannelInfoViewModel: ObservableObject, ChatChannelControllerDele
}
}

public var leaveButtonTitle: String {
open var leaveButtonTitle: String {
if channel.isDirectMessageChannel {
L10n.Alert.Actions.deleteChannelTitle
} else {
L10n.Alert.Actions.leaveGroupTitle
}
}

public var leaveConversationDescription: String {
open var leaveConversationDescription: String {
if channel.isDirectMessageChannel {
L10n.Alert.Actions.deleteChannelMessage
} else {
Expand Down Expand Up @@ -180,7 +184,7 @@ open class ChatChannelInfoViewModel: ObservableObject, ChatChannelControllerDele
loadAdditionalUsers()
}

public func leaveConversationTapped(completion: @escaping () -> Void) {
open func leaveConversationTapped(completion: @escaping () -> Void) {
if !channel.isDirectMessageChannel {
removeUserFromConversation(completion: completion)
} else {
Expand Down Expand Up @@ -225,9 +229,7 @@ open class ChatChannelInfoViewModel: ObservableObject, ChatChannelControllerDele
addUsersShown = false
}

// MARK: - private

private func removeUserFromConversation(completion: @escaping () -> Void) {
public func removeUserFromConversation(completion: @escaping () -> Void) {
guard let userId = chatClient.currentUserId else { return }
channelController.removeMembers(userIds: [userId]) { [weak self] error in
if error != nil {
Expand All @@ -237,7 +239,9 @@ open class ChatChannelInfoViewModel: ObservableObject, ChatChannelControllerDele
}
}
}


// MARK: - private

private func deleteChannel(completion: @escaping () -> Void) {
channelController.deleteChannel { [weak self] error in
if error != nil {
Expand Down Expand Up @@ -278,11 +282,11 @@ open class ChatChannelInfoViewModel: ObservableObject, ChatChannelControllerDele
}

open func participantActions(for participant: ParticipantInfo) -> [ParticipantAction] {
var actions = [ParticipantAction]()
var actions = [ParticipantAction?]()

var directMessageAction = ParticipantAction(
title: L10n.Channel.Item.sendDirectMessage,
iconName: "message.circle.fill",
iconName: "message",
action: {},
confirmationPopup: nil,
isDestructive: false
Expand Down Expand Up @@ -339,7 +343,7 @@ open class ChatChannelInfoViewModel: ObservableObject, ChatChannelControllerDele

actions.append(cancel)

return actions
return actions.compactMap({$0})
}

public func muteAction(
Expand Down Expand Up @@ -403,11 +407,11 @@ open class ChatChannelInfoViewModel: ObservableObject, ChatChannelControllerDele
return unmuteUser
}

public func removeUserAction(
open func removeUserAction(
participant: ParticipantInfo,
onDismiss: @escaping () -> Void,
onError: @escaping (Error) -> Void
) -> ParticipantAction {
) -> ParticipantAction? {
Copy link
Member

Choose a reason for hiding this comment

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

This is a breaking change of the SDK, so we can't change this at the moment. Can you find an alternative?

let action = { [weak self] in
guard let self else {
onError(ClientError.Unexpected("Self is nil"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@ public struct ChatInfoParticipantsView<Factory: ViewFactory>: View {
let factory: Factory
var participants: [ParticipantInfo]
var onItemAppear: (ParticipantInfo) -> Void
private let channel: ChatChannel

public init(
factory: Factory = DefaultViewFactory.shared,
participants: [ParticipantInfo],
channel: ChatChannel,
onItemAppear: @escaping (ParticipantInfo) -> Void,
selectedParticipant: Binding<ParticipantInfo?> = .constant(nil)
) {
self.factory = factory
self.participants = participants
self.onItemAppear = onItemAppear
self.channel = channel
_selectedParticipant = selectedParticipant
}

Expand All @@ -41,14 +44,24 @@ public struct ChatInfoParticipantsView<Factory: ViewFactory>: View {
)
factory.makeMessageAvatarView(for: displayInfo)

VStack(alignment: .leading, spacing: 4) {
Text(participant.displayName)
.lineLimit(1)
.font(fonts.bodyBold)
Text(participant.onlineInfoText)
.font(fonts.footnote)
.foregroundColor(Color(colors.textLowEmphasis))
HStack(alignment: .center) {
VStack(alignment: .leading, spacing: 4) {
Text(participant.displayName)
.lineLimit(1)
.font(fonts.bodyBold)
Text(participant.onlineInfoText)
.font(fonts.footnote)
.foregroundColor(Color(colors.textLowEmphasis))
}
Spacer()

if channel.createdBy?.id == participant.chatUser.id {
Text(L10n.chatGroupInfoOwner)
.font(fonts.footnote)
.foregroundColor(Color(colors.textLowEmphasis))
}
}

Spacer()
}
.padding(.all, 8)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,19 +132,15 @@ public struct ChatChannelSwipeableListItem<Factory: ViewFactory, ChannelListItem
}

private var showTrailingSwipeActions: Bool {
#if DEBUG
let view = factory.makeTrailingSwipeActionsView(
channel: channel,
offsetX: offsetX,
buttonWidth: buttonWidth,
swipedChannelId: $swipedChannelId,
leftButtonTapped: trailingLeftButtonTapped,
rightButtonTapped: trailingRightButtonTapped
)
return !(view is EmptyView)
#else
return !(trailingSwipeActions is EmptyView)
#endif
) as? TrailingSwipeActionsView
return view?.hasActions ?? true
Comment on lines +142 to +143
Copy link
Member

Choose a reason for hiding this comment

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

Why are we changing this?

}

private var leadingSwipeActions: some View {
Expand Down Expand Up @@ -276,20 +272,27 @@ public struct TrailingSwipeActionsView: View {
var buttonWidth: CGFloat
var leftButtonTapped: (ChatChannel) -> Void
var rightButtonTapped: (ChatChannel) -> Void

var hasActions: Bool {
channel.ownCapabilities.contains(.moreOptionsChannel) ||
channel.ownCapabilities.contains(.deleteChannel)
}

public var body: some View {
HStack {
Spacer()
ZStack {
HStack(spacing: 0) {
ActionItemButton(imageName: "ellipsis", action: {
withAnimation {
leftButtonTapped(channel)
}
})
.frame(width: buttonWidth)
.foregroundColor(Color(colors.text))
.background(Color(colors.background1))
if channel.ownCapabilities.contains(.moreOptionsChannel) {
ActionItemButton(imageName: "ellipsis", action: {
withAnimation {
leftButtonTapped(channel)
}
})
.frame(width: buttonWidth)
.foregroundColor(Color(colors.text))
.background(Color(colors.background1))
}

if channel.ownCapabilities.contains(.deleteChannel) {
ActionItemButton(imageName: "trash", action: {
Expand Down Expand Up @@ -331,3 +334,11 @@ public struct ActionItemButton: View {
}
}
}

/*
If you wanna add this button to 'ChannelCapability' of ChatChannel manual
*/
extension ChannelCapability {
/// Ability to add more Options the channel.
public static let moreOptionsChannel: Self = "moreOptions-channel"
Copy link
Member

Choose a reason for hiding this comment

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

Where did you see this capability? I can't find it here: https://getstream.io/chat/docs/ios-swift/channel_capabilities/

Copy link
Author

@KhaledKamalA KhaledKamalA Oct 29, 2025

Choose a reason for hiding this comment

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

I added .moreOptionsChannel as a custom capability to control the visibility of the “more options” button when swiping on a channel in the list.
It’s not part of Stream’s built-in capabilities — it’s used internally to show or hide the action button based on our specific use case.

Is there a recommended or built-in way in the Stream SDK to achieve this behavior (conditionally hiding this button) without introducing a custom capability?

UI Update – Channel More Options Button

After Change

After Change Screenshot

Before Change

Simulator Screenshot - iPhone 15 - 2025-10-29 at 12 07 29

}
2 changes: 2 additions & 0 deletions Sources/StreamChatSwiftUI/Generated/L10n.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import Foundation
// MARK: - Strings

internal enum L10n {
/// Owner
internal static var chatGroupInfoOwner: String { L10n.tr("Localizable", "chat_group_info_owner") }
/// %d of %d
internal static func currentSelection(_ p1: Int, _ p2: Int) -> String {
return L10n.tr("Localizable", "current-selection", p1, p2)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,5 @@
"thread.error.message" = "Error loading threads";
"thread.no-content.message" = "No threads here yet...";
"thread.item.replied-to" = "replied to: %@";

"chat_group_info_owner" = "Owner";