Skip to content

Commit 745bf5a

Browse files
authored
Add utils.channelListConfig.channelItemMutedLayoutStyle (#881)
* Disable the channel pinning feature by default in the Demo App * Add `utils.channelListConfig.channelItemMutedLayoutStyle` to configure the mute styling * Update CHANGELOG.md
1 parent 218e875 commit 745bf5a

13 files changed

+195
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66
### ✅ Added
77
- Add support for customising the `MessageAvatarView` placeholder [#878](https://github.com/GetStream/stream-chat-swiftui/pull/878)
88
- Add `ViewFactory.makeVideoPlayerFooterView` to customize video player footer [#879](https://github.com/GetStream/stream-chat-swiftui/pull/879)
9+
- Add `utils.channelListConfig.channelItemMutedLayoutStyle` [#881](https://github.com/GetStream/stream-chat-swiftui/pull/881)
910

1011
# [4.81.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.81.0)
1112
_July 03, 2025_

DemoAppSwiftUI/AppConfiguration/AppConfiguration.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ final class AppConfiguration {
1111

1212
/// The translation language to set on connect.
1313
var translationLanguage: TranslationLanguage?
14+
/// A flag indicating whether the channel pinning feature is enabled.
15+
var isChannelPinningFeatureEnabled = false
1416
}

DemoAppSwiftUI/AppConfiguration/AppConfigurationView.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,20 @@ import Combine
66
import SwiftUI
77

88
struct AppConfigurationView: View {
9+
var channelPinningEnabled: Binding<Bool> = Binding {
10+
AppConfiguration.default.isChannelPinningFeatureEnabled
11+
} set: { newValue in
12+
AppConfiguration.default.isChannelPinningFeatureEnabled = newValue
13+
}
14+
915
var body: some View {
1016
NavigationView {
1117
List {
1218
Section("Connect User Configuration") {
1319
NavigationLink("Translation") {
1420
AppConfigurationTranslationView()
1521
}
22+
Toggle("Channel Pinning", isOn: channelPinningEnabled)
1623
}
1724
}
1825
.navigationBarTitleDisplayMode(.inline)

DemoAppSwiftUI/AppDelegate.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ class AppDelegate: NSObject, UIApplicationDelegate {
6969

7070
let utils = Utils(
7171
channelListConfig: ChannelListConfig(
72-
messageRelativeDateFormatEnabled: true
72+
messageRelativeDateFormatEnabled: true,
73+
channelItemMutedStyle: .afterChannelName
7374
),
7475
messageListConfig: MessageListConfig(
7576
messageDisplayOptions: .init(showOriginalTranslatedButton: true),

DemoAppSwiftUI/DemoAppSwiftUIApp.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,13 @@ extension AppState {
147147
guard let currentUserId = chatClient.currentUserId else { fatalError("Not logged in") }
148148
switch identifier {
149149
case .initial:
150+
return ChannelListQuery(
151+
filter: .containMembers(userIds: [currentUserId]),
152+
sort: [
153+
Sorting(key: .default)
154+
]
155+
)
156+
case .initial where AppConfiguration.default.isChannelPinningFeatureEnabled:
150157
return ChannelListQuery(
151158
filter: .containMembers(userIds: [currentUserId]),
152159
sort: [

DemoAppSwiftUI/PinChannelHelpers.swift

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -150,15 +150,27 @@ struct DemoAppChatChannelNavigatableListItem<ChannelDestination: View>: View {
150150

151151
public var body: some View {
152152
ZStack {
153-
DemoAppChatChannelListItem(
154-
channel: channel,
155-
channelName: channelName,
156-
injectedChannelInfo: injectedChannelInfo,
157-
avatar: avatar,
158-
onlineIndicatorShown: onlineIndicatorShown,
159-
disabled: disabled,
160-
onItemTap: onItemTap
161-
)
153+
if AppConfiguration.default.isChannelPinningFeatureEnabled {
154+
DemoAppChatChannelListItem(
155+
channel: channel,
156+
channelName: channelName,
157+
injectedChannelInfo: injectedChannelInfo,
158+
avatar: avatar,
159+
onlineIndicatorShown: onlineIndicatorShown,
160+
disabled: disabled,
161+
onItemTap: onItemTap
162+
)
163+
} else {
164+
ChatChannelListItem(
165+
channel: channel,
166+
channelName: channelName,
167+
injectedChannelInfo: injectedChannelInfo,
168+
avatar: avatar,
169+
onlineIndicatorShown: onlineIndicatorShown,
170+
disabled: disabled,
171+
onItemTap: onItemTap
172+
)
173+
}
162174

163175
NavigationLink(
164176
tag: channel.channelSelectionInfo,

DemoAppSwiftUI/ViewFactoryExamples.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,13 @@ class DemoAppFactory: ViewFactory {
3131
onError: onError
3232
)
3333
let archiveChannel = archiveChannelAction(for: channel, onDismiss: onDismiss, onError: onError)
34-
let pinChannel = pinChannelAction(for: channel, onDismiss: onDismiss, onError: onError)
3534
actions.insert(archiveChannel, at: actions.count - 2)
36-
actions.insert(pinChannel, at: actions.count - 2)
35+
36+
if AppConfiguration.default.isChannelPinningFeatureEnabled {
37+
let pinChannel = pinChannelAction(for: channel, onDismiss: onDismiss, onError: onError)
38+
actions.insert(pinChannel, at: actions.count - 2)
39+
}
40+
3741
return actions
3842
}
3943

Sources/StreamChatSwiftUI/ChatChannelList/ChannelListConfig.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import Foundation
88
public struct ChannelListConfig {
99
public init(
1010
messageRelativeDateFormatEnabled: Bool = false,
11-
showChannelListDividerOnLastItem: Bool = true
11+
showChannelListDividerOnLastItem: Bool = true,
12+
channelItemMutedStyle: ChannelItemMutedLayoutStyle = .default
1213
) {
1314
self.messageRelativeDateFormatEnabled = messageRelativeDateFormatEnabled
1415
self.showChannelListDividerOnLastItem = showChannelListDividerOnLastItem
16+
self.channelItemMutedStyle = channelItemMutedStyle
1517
}
1618

1719
/// If true, the timestamp format depends on the time passed.
@@ -24,4 +26,7 @@ public struct ChannelListConfig {
2426
///
2527
/// By default, all items in the channel list have a divider, including the last item.
2628
public var showChannelListDividerOnLastItem: Bool
29+
30+
/// The style for the channel item when it is muted.
31+
public var channelItemMutedStyle: ChannelItemMutedLayoutStyle = .default
2732
}

Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListItem.swift

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,20 @@ public struct ChatChannelListItem<Factory: ViewFactory>: View {
5454

5555
VStack(alignment: .leading, spacing: 4) {
5656
HStack {
57-
ChatTitleView(name: channelName)
57+
HStack(spacing: 6) {
58+
ChatTitleView(name: channelName)
59+
if channel.isMuted, mutedLayoutStyle == .afterChannelName {
60+
mutedIcon
61+
.frame(maxHeight: 14)
62+
.padding(.bottom, -2)
63+
}
64+
}
5865

5966
Spacer()
6067

68+
if channel.isMuted, mutedLayoutStyle == .topRightCorner {
69+
mutedIcon
70+
}
6171
if injectedChannelInfo == nil && channel.unreadCount != .noUnread {
6272
UnreadIndicatorView(
6373
unreadCount: channel.unreadCount.messages
@@ -93,13 +103,14 @@ public struct ChatChannelListItem<Factory: ViewFactory>: View {
93103
.id("\(channel.id)-base")
94104
}
95105

106+
private var mutedLayoutStyle: ChannelItemMutedLayoutStyle {
107+
utils.channelListConfig.channelItemMutedStyle
108+
}
109+
96110
private var subtitleView: some View {
97111
HStack(spacing: 4) {
98-
if let image = image {
99-
Image(uiImage: image)
100-
.customizable()
101-
.frame(maxHeight: 12)
102-
.foregroundColor(Color(colors.subtitleText))
112+
if channel.isMuted, mutedLayoutStyle == .default {
113+
mutedIcon
103114
} else {
104115
if channel.shouldShowTypingIndicator {
105116
TypingIndicatorView()
@@ -113,13 +124,40 @@ public struct ChatChannelListItem<Factory: ViewFactory>: View {
113124
SubtitleText(text: draftText)
114125
}
115126
} else {
116-
SubtitleText(text: injectedChannelInfo?.subtitle ?? channel.subtitleText)
127+
SubtitleText(text: subtitleText)
117128
}
118129
Spacer()
119130
}
120131
.accessibilityIdentifier("subtitleView")
121132
}
122133

134+
private var subtitleText: String {
135+
if let injectedSubtitle = injectedChannelInfo?.subtitle {
136+
return injectedSubtitle
137+
}
138+
if mutedLayoutStyle != .default {
139+
return channelSubtitleText
140+
}
141+
return channel.subtitleText
142+
}
143+
144+
private var channelSubtitleText: String {
145+
if channel.shouldShowTypingIndicator {
146+
return channel.typingIndicatorString(currentUserId: chatClient.currentUserId)
147+
} else if let previewMessageText = channel.previewMessageText {
148+
return previewMessageText
149+
} else {
150+
return L10n.Channel.Item.emptyMessages
151+
}
152+
}
153+
154+
private var mutedIcon: some View {
155+
Image(uiImage: images.muted)
156+
.customizable()
157+
.frame(maxHeight: 12)
158+
.foregroundColor(Color(colors.subtitleText))
159+
}
160+
123161
private var shouldShowReadEvents: Bool {
124162
if let message = channel.latestMessages.first,
125163
message.isSentByCurrentUser,
@@ -345,3 +383,23 @@ extension ChatChannel {
345383
}
346384
}
347385
}
386+
387+
/// The style for the muted icon in the channel list item.
388+
public struct ChannelItemMutedLayoutStyle: Hashable {
389+
let identifier: String
390+
391+
init(_ identifier: String) {
392+
self.identifier = identifier
393+
}
394+
395+
/// The default style shows the muted icon and the text "channel is muted" as the subtitle text.
396+
public static var `default`: ChannelItemMutedLayoutStyle = .init("default")
397+
398+
/// This style shows the muted icon at the top right corner of the channel item.
399+
/// The subtitle text shows the last message preview text.
400+
public static var topRightCorner: ChannelItemMutedLayoutStyle = .init("topRightCorner")
401+
402+
/// This style shows the muted icon after the channel name.
403+
/// The subtitle text shows the last message preview text.
404+
public static var afterChannelName: ChannelItemMutedLayoutStyle = .init("afterChannelName")
405+
}

StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListItemView_Tests.swift

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,83 @@ final class ChatChannelListItemView_Tests: StreamChatTestCase {
9595
// Then
9696
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
9797
}
98-
98+
99+
func test_channelListItem_muted_defaultStyle() throws {
100+
// Given
101+
let message = try mockPollMessage(isSentByCurrentUser: false)
102+
let channel = ChatChannel.mock(
103+
cid: .unique,
104+
latestMessages: [message],
105+
muteDetails: .init(createdAt: .unique, updatedAt: .unique, expiresAt: nil),
106+
previewMessage: message
107+
)
108+
109+
// When
110+
let view = ChatChannelListItem(
111+
channel: channel,
112+
channelName: "Test",
113+
avatar: .circleImage,
114+
onlineIndicatorShown: true,
115+
onItemTap: { _ in }
116+
)
117+
.frame(width: defaultScreenSize.width)
118+
119+
// Then
120+
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
121+
}
122+
123+
func test_channelListItem_muted_channelNameStyle() throws {
124+
// Given
125+
streamChat?.utils.channelListConfig.channelItemMutedStyle = .afterChannelName
126+
let message = try mockPollMessage(isSentByCurrentUser: false)
127+
let channel = ChatChannel.mock(
128+
cid: .unique,
129+
unreadCount: .mock(messages: 4),
130+
latestMessages: [message],
131+
muteDetails: .init(createdAt: .unique, updatedAt: .unique, expiresAt: nil),
132+
previewMessage: message
133+
)
134+
135+
// When
136+
let view = ChatChannelListItem(
137+
channel: channel,
138+
channelName: "Test",
139+
avatar: .circleImage,
140+
onlineIndicatorShown: true,
141+
onItemTap: { _ in }
142+
)
143+
.frame(width: defaultScreenSize.width)
144+
145+
// Then
146+
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
147+
}
148+
149+
func test_channelListItem_muted_topRightCornerStyle() throws {
150+
// Given
151+
streamChat?.utils.channelListConfig.channelItemMutedStyle = .topRightCorner
152+
let message = try mockPollMessage(isSentByCurrentUser: false)
153+
let channel = ChatChannel.mock(
154+
cid: .unique,
155+
unreadCount: .mock(messages: 4),
156+
latestMessages: [message],
157+
muteDetails: .init(createdAt: .unique, updatedAt: .unique, expiresAt: nil),
158+
previewMessage: message
159+
)
160+
161+
// When
162+
let view = ChatChannelListItem(
163+
channel: channel,
164+
channelName: "Test",
165+
avatar: .circleImage,
166+
onlineIndicatorShown: true,
167+
onItemTap: { _ in }
168+
)
169+
.frame(width: defaultScreenSize.width)
170+
171+
// Then
172+
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
173+
}
174+
99175
func test_channelListItem_giphyMessageLatestButPreviewIsAnotherMessage() throws {
100176
// Given
101177
let previewMessage = try mockImageMessage(text: "Hi!", isSentByCurrentUser: true)
@@ -345,7 +421,7 @@ final class ChatChannelListItemView_Tests: StreamChatTestCase {
345421
isSentByCurrentUser: isSentByCurrentUser
346422
)
347423
}
348-
424+
349425
private func mockVideoMessage(text: String, isSentByCurrentUser: Bool) throws -> ChatMessage {
350426
.mock(
351427
id: .unique,

0 commit comments

Comments
 (0)