Skip to content

Add utils.channelListConfig.channelItemMutedLayoutStyle #881

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### ✅ Added
- Add support for customising the `MessageAvatarView` placeholder [#878](https://github.com/GetStream/stream-chat-swiftui/pull/878)
- Add `ViewFactory.makeVideoPlayerFooterView` to customize video player footer [#879](https://github.com/GetStream/stream-chat-swiftui/pull/879)
- Add `utils.channelListConfig.channelItemMutedLayoutStyle` [#881](https://github.com/GetStream/stream-chat-swiftui/pull/881)

# [4.81.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.81.0)
_July 03, 2025_
Expand Down
2 changes: 2 additions & 0 deletions DemoAppSwiftUI/AppConfiguration/AppConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ final class AppConfiguration {

/// The translation language to set on connect.
var translationLanguage: TranslationLanguage?
/// A flag indicating whether the channel pinning feature is enabled.
var isChannelPinningFeatureEnabled = false
}
7 changes: 7 additions & 0 deletions DemoAppSwiftUI/AppConfiguration/AppConfigurationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@ import Combine
import SwiftUI

struct AppConfigurationView: View {
var channelPinningEnabled: Binding<Bool> = Binding {
AppConfiguration.default.isChannelPinningFeatureEnabled
} set: { newValue in
AppConfiguration.default.isChannelPinningFeatureEnabled = newValue
}

var body: some View {
NavigationView {
List {
Section("Connect User Configuration") {
NavigationLink("Translation") {
AppConfigurationTranslationView()
}
Toggle("Channel Pinning", isOn: channelPinningEnabled)
}
}
.navigationBarTitleDisplayMode(.inline)
Expand Down
3 changes: 2 additions & 1 deletion DemoAppSwiftUI/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ class AppDelegate: NSObject, UIApplicationDelegate {

let utils = Utils(
channelListConfig: ChannelListConfig(
messageRelativeDateFormatEnabled: true
messageRelativeDateFormatEnabled: true,
channelItemMutedStyle: .afterChannelName
),
messageListConfig: MessageListConfig(
messageDisplayOptions: .init(showOriginalTranslatedButton: true),
Expand Down
7 changes: 7 additions & 0 deletions DemoAppSwiftUI/DemoAppSwiftUIApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ extension AppState {
guard let currentUserId = chatClient.currentUserId else { fatalError("Not logged in") }
switch identifier {
case .initial:
return ChannelListQuery(
filter: .containMembers(userIds: [currentUserId]),
sort: [
Sorting(key: .default)
]
)
case .initial where AppConfiguration.default.isChannelPinningFeatureEnabled:
return ChannelListQuery(
filter: .containMembers(userIds: [currentUserId]),
sort: [
Expand Down
30 changes: 21 additions & 9 deletions DemoAppSwiftUI/PinChannelHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,15 +151,27 @@ struct DemoAppChatChannelNavigatableListItem<ChannelDestination: View>: View {

public var body: some View {
ZStack {
DemoAppChatChannelListItem(
channel: channel,
channelName: channelName,
injectedChannelInfo: injectedChannelInfo,
avatar: avatar,
onlineIndicatorShown: onlineIndicatorShown,
disabled: disabled,
onItemTap: onItemTap
)
if AppConfiguration.default.isChannelPinningFeatureEnabled {
DemoAppChatChannelListItem(
channel: channel,
channelName: channelName,
injectedChannelInfo: injectedChannelInfo,
avatar: avatar,
onlineIndicatorShown: onlineIndicatorShown,
disabled: disabled,
onItemTap: onItemTap
)
} else {
ChatChannelListItem(
channel: channel,
channelName: channelName,
injectedChannelInfo: injectedChannelInfo,
avatar: avatar,
onlineIndicatorShown: onlineIndicatorShown,
disabled: disabled,
onItemTap: onItemTap
)
}

NavigationLink(
tag: channel.channelSelectionInfo,
Expand Down
8 changes: 6 additions & 2 deletions DemoAppSwiftUI/ViewFactoryExamples.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,13 @@ class DemoAppFactory: ViewFactory {
onError: onError
)
let archiveChannel = archiveChannelAction(for: channel, onDismiss: onDismiss, onError: onError)
let pinChannel = pinChannelAction(for: channel, onDismiss: onDismiss, onError: onError)
actions.insert(archiveChannel, at: actions.count - 2)
actions.insert(pinChannel, at: actions.count - 2)

if AppConfiguration.default.isChannelPinningFeatureEnabled {
let pinChannel = pinChannelAction(for: channel, onDismiss: onDismiss, onError: onError)
actions.insert(pinChannel, at: actions.count - 2)
}

return actions
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import Foundation
public struct ChannelListConfig {
public init(
messageRelativeDateFormatEnabled: Bool = false,
showChannelListDividerOnLastItem: Bool = true
showChannelListDividerOnLastItem: Bool = true,
channelItemMutedStyle: ChannelItemMutedLayoutStyle = .default
) {
self.messageRelativeDateFormatEnabled = messageRelativeDateFormatEnabled
self.showChannelListDividerOnLastItem = showChannelListDividerOnLastItem
self.channelItemMutedStyle = channelItemMutedStyle
}

/// If true, the timestamp format depends on the time passed.
Expand All @@ -24,4 +26,7 @@ public struct ChannelListConfig {
///
/// By default, all items in the channel list have a divider, including the last item.
public var showChannelListDividerOnLastItem: Bool

/// The style for the channel item when it is muted.
public var channelItemMutedStyle: ChannelItemMutedLayoutStyle = .default
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import SwiftUI

/// View for the channel list item.
public struct ChatChannelListItem<Factory: ViewFactory>: View {

@Injected(\.fonts) private var fonts
@Injected(\.colors) private var colors
@Injected(\.utils) private var utils
Expand Down Expand Up @@ -55,10 +54,20 @@ public struct ChatChannelListItem<Factory: ViewFactory>: View {

VStack(alignment: .leading, spacing: 4) {
HStack {
ChatTitleView(name: channelName)
HStack(spacing: 6) {
ChatTitleView(name: channelName)
if channel.isMuted, mutedLayoutStyle == .afterChannelName {
mutedIcon
.frame(maxHeight: 14)
.padding(.bottom, -2)
}
}

Spacer()

if channel.isMuted, mutedLayoutStyle == .topRightCorner {
mutedIcon
}
if injectedChannelInfo == nil && channel.unreadCount != .noUnread {
UnreadIndicatorView(
unreadCount: channel.unreadCount.messages
Expand Down Expand Up @@ -94,13 +103,14 @@ public struct ChatChannelListItem<Factory: ViewFactory>: View {
.id("\(channel.id)-base")
}

private var mutedLayoutStyle: ChannelItemMutedLayoutStyle {
utils.channelListConfig.channelItemMutedStyle
}

private var subtitleView: some View {
HStack(spacing: 4) {
if let image = image {
Image(uiImage: image)
.customizable()
.frame(maxHeight: 12)
.foregroundColor(Color(colors.subtitleText))
if channel.isMuted, mutedLayoutStyle == .default {
mutedIcon
} else {
if channel.shouldShowTypingIndicator {
TypingIndicatorView()
Expand All @@ -114,13 +124,40 @@ public struct ChatChannelListItem<Factory: ViewFactory>: View {
SubtitleText(text: draftText)
}
} else {
SubtitleText(text: injectedChannelInfo?.subtitle ?? channel.subtitleText)
SubtitleText(text: subtitleText)
}
Spacer()
}
.accessibilityIdentifier("subtitleView")
}

private var subtitleText: String {
if let injectedSubtitle = injectedChannelInfo?.subtitle {
return injectedSubtitle
}
if mutedLayoutStyle != .default {
return channelSubtitleText
}
return channel.subtitleText
}

private var channelSubtitleText: String {
if channel.shouldShowTypingIndicator {
return channel.typingIndicatorString(currentUserId: chatClient.currentUserId)
} else if let previewMessageText = channel.previewMessageText {
return previewMessageText
} else {
return L10n.Channel.Item.emptyMessages
}
}

private var mutedIcon: some View {
Image(uiImage: images.muted)
.customizable()
.frame(maxHeight: 12)
.foregroundColor(Color(colors.subtitleText))
}

private var shouldShowReadEvents: Bool {
if let message = channel.latestMessages.first,
message.isSentByCurrentUser,
Expand Down Expand Up @@ -347,3 +384,23 @@ extension ChatChannel {
}
}
}

/// The style for the muted icon in the channel list item.
public struct ChannelItemMutedLayoutStyle: Hashable {
let identifier: String

init(_ identifier: String) {
self.identifier = identifier
}

/// The default style shows the muted icon and the text "channel is muted" as the subtitle text.
public static var `default`: ChannelItemMutedLayoutStyle = .init("default")

/// This style shows the muted icon at the top right corner of the channel item.
/// The subtitle text shows the last message preview text.
public static var topRightCorner: ChannelItemMutedLayoutStyle = .init("topRightCorner")

/// This style shows the muted icon after the channel name.
/// The subtitle text shows the last message preview text.
public static var afterChannelName: ChannelItemMutedLayoutStyle = .init("afterChannelName")
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,83 @@ final class ChatChannelListItemView_Tests: StreamChatTestCase {
// Then
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
}


func test_channelListItem_muted_defaultStyle() throws {
// Given
let message = try mockPollMessage(isSentByCurrentUser: false)
let channel = ChatChannel.mock(
cid: .unique,
latestMessages: [message],
muteDetails: .init(createdAt: .unique, updatedAt: .unique, expiresAt: nil),
previewMessage: message
)

// When
let view = ChatChannelListItem(
channel: channel,
channelName: "Test",
avatar: .circleImage,
onlineIndicatorShown: true,
onItemTap: { _ in }
)
.frame(width: defaultScreenSize.width)

// Then
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
}

func test_channelListItem_muted_channelNameStyle() throws {
// Given
streamChat?.utils.channelListConfig.channelItemMutedStyle = .afterChannelName
let message = try mockPollMessage(isSentByCurrentUser: false)
let channel = ChatChannel.mock(
cid: .unique,
unreadCount: .mock(messages: 4),
latestMessages: [message],
muteDetails: .init(createdAt: .unique, updatedAt: .unique, expiresAt: nil),
previewMessage: message
)

// When
let view = ChatChannelListItem(
channel: channel,
channelName: "Test",
avatar: .circleImage,
onlineIndicatorShown: true,
onItemTap: { _ in }
)
.frame(width: defaultScreenSize.width)

// Then
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
}

func test_channelListItem_muted_topRightCornerStyle() throws {
// Given
streamChat?.utils.channelListConfig.channelItemMutedStyle = .topRightCorner
let message = try mockPollMessage(isSentByCurrentUser: false)
let channel = ChatChannel.mock(
cid: .unique,
unreadCount: .mock(messages: 4),
latestMessages: [message],
muteDetails: .init(createdAt: .unique, updatedAt: .unique, expiresAt: nil),
previewMessage: message
)

// When
let view = ChatChannelListItem(
channel: channel,
channelName: "Test",
avatar: .circleImage,
onlineIndicatorShown: true,
onItemTap: { _ in }
)
.frame(width: defaultScreenSize.width)

// Then
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
}

func test_channelListItem_giphyMessageLatestButPreviewIsAnotherMessage() throws {
// Given
let previewMessage = try mockImageMessage(text: "Hi!", isSentByCurrentUser: true)
Expand Down Expand Up @@ -346,7 +422,7 @@ final class ChatChannelListItemView_Tests: StreamChatTestCase {
isSentByCurrentUser: isSentByCurrentUser
)
}

private func mockVideoMessage(text: String, isSentByCurrentUser: Bool) throws -> ChatMessage {
.mock(
id: .unique,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading