Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### ✅ Added
- Add extra data to user display info [#819](https://github.com/GetStream/stream-chat-swiftui/pull/819)
- Make message spacing in message list configurable [#830](https://github.com/GetStream/stream-chat-swiftui/pull/830)
- Show time, relative date, weekday, or short date for last message in channel list and search [#833](https://github.com/GetStream/stream-chat-swiftui/pull/833)
- Set `ChannelListConfig.messageRelativeDateFormatEnabled` to true for enabling it
### 🐞 Fixed
- Fix swipe to reply enabled when quoting a message is disabled [#824](https://github.com/GetStream/stream-chat-swiftui/pull/824)
- Fix mark unread action not removed when read events are disabled [#823](https://github.com/GetStream/stream-chat-swiftui/pull/823)
Expand Down
3 changes: 3 additions & 0 deletions DemoAppSwiftUI/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ class AppDelegate: NSObject, UIApplicationDelegate {
#endif

let utils = Utils(
channelListConfig: ChannelListConfig(
messageRelativeDateFormatEnabled: true
),
messageListConfig: MessageListConfig(
dateIndicatorPlacement: .messageList,
userBlockingEnabled: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// Copyright © 2025 Stream.io Inc. All rights reserved.
//

import Foundation

/// A configuration for channel lists.
public struct ChannelListConfig {
public init(messageRelativeDateFormatEnabled: Bool = false) {
self.messageRelativeDateFormatEnabled = messageRelativeDateFormatEnabled
}

/// If true, the timestamp format depends on the time passed.
///
/// Different date formats are used for today, yesterday, last 7 days, and older dates.
public var messageRelativeDateFormatEnabled: Bool
}
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,11 @@ extension ChatChannel {

public var timestampText: String {
if let lastMessageAt = lastMessageAt {
return InjectedValues[\.utils].dateFormatter.string(from: lastMessageAt)
let utils = InjectedValues[\.utils]
let formatter = utils.channelListConfig.messageRelativeDateFormatEnabled ?
utils.messageRelativeDateFormatter :
utils.dateFormatter
return formatter.string(from: lastMessageAt)
} else {
return ""
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,10 @@ struct SearchResultItem<Factory: ViewFactory, ChannelDestination: View>: View {

private var timestampText: String {
if let lastMessageAt = searchResult.channel.lastMessageAt {
return utils.dateFormatter.string(from: lastMessageAt)
let formatter = utils.channelListConfig.messageRelativeDateFormatEnabled ?
utils.messageRelativeDateFormatter :
utils.dateFormatter
return formatter.string(from: lastMessageAt)
} else {
return ""
}
Expand Down
10 changes: 10 additions & 0 deletions Sources/StreamChatSwiftUI/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ public class Utils {
var markdownFormatter = MarkdownFormatter()

public var dateFormatter: DateFormatter

/// Date formatter where the format depends on the time passed.
///
/// - SeeAlso: ``ChannelListConfig/messageRelativeDateFormatEnabled``.
public var messageRelativeDateFormatter: DateFormatter
public var videoPreviewLoader: VideoPreviewLoader
public var imageLoader: ImageLoading
public var imageCDN: ImageCDN
Expand All @@ -25,6 +30,7 @@ public class Utils {
public var messageTypeResolver: MessageTypeResolving
public var messageActionsResolver: MessageActionsResolving
public var commandsConfig: CommandsConfig
public var channelListConfig: ChannelListConfig
public var messageListConfig: MessageListConfig
public var composerConfig: ComposerConfig
public var pollsConfig: PollsConfig
Expand Down Expand Up @@ -69,6 +75,7 @@ public class Utils {

public init(
dateFormatter: DateFormatter = .makeDefault(),
messageRelativeDateFormatter: DateFormatter = MessageRelativeDateFormatter(),
videoPreviewLoader: VideoPreviewLoader = DefaultVideoPreviewLoader(),
imageLoader: ImageLoading = NukeImageLoader(),
imageCDN: ImageCDN = StreamImageCDN(),
Expand All @@ -79,6 +86,7 @@ public class Utils {
messageTypeResolver: MessageTypeResolving = MessageTypeResolver(),
messageActionResolver: MessageActionsResolving = MessageActionsResolver(),
commandsConfig: CommandsConfig = DefaultCommandsConfig(),
channelListConfig: ChannelListConfig = ChannelListConfig(),
messageListConfig: MessageListConfig = MessageListConfig(),
composerConfig: ComposerConfig = ComposerConfig(),
pollsConfig: PollsConfig = PollsConfig(),
Expand All @@ -93,6 +101,7 @@ public class Utils {
shouldSyncChannelControllerOnAppear: @escaping (ChatChannelController) -> Bool = { _ in true }
) {
self.dateFormatter = dateFormatter
self.messageRelativeDateFormatter = messageRelativeDateFormatter
self.videoPreviewLoader = videoPreviewLoader
self.imageLoader = imageLoader
self.imageCDN = imageCDN
Expand All @@ -105,6 +114,7 @@ public class Utils {
self.messageTypeResolver = messageTypeResolver
messageActionsResolver = messageActionResolver
self.commandsConfig = commandsConfig
self.channelListConfig = channelListConfig
self.messageListConfig = messageListConfig
self.composerConfig = composerConfig
self.snapshotCreator = snapshotCreator
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//
// Copyright © 2025 Stream.io Inc. All rights reserved.
//

import Foundation

/// A formatter that converts message timestamps to a format which depends on the time passed.
public final class MessageRelativeDateFormatter: DateFormatter, @unchecked Sendable {
override public init() {
super.init()
locale = .autoupdatingCurrent
dateStyle = .short
timeStyle = .none
}

@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override public func string(from date: Date) -> String {
if calendar.isDateInToday(date) {
return todayFormatter.string(from: date)
}
if calendar.isDateInYesterday(date) {
return yesterdayFormatter.string(from: date)
}
if calendar.isDateInLastWeek(date) {
return weekdayFormatter.string(from: date)
}

return super.string(from: date)
}

var todayFormatter: DateFormatter {
InjectedValues[\.utils].dateFormatter
}

let yesterdayFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = .autoupdatingCurrent
formatter.dateStyle = .short
formatter.timeStyle = .none
formatter.doesRelativeDateFormatting = true
return formatter
}()

let weekdayFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = .autoupdatingCurrent
formatter.setLocalizedDateFormatFromTemplate("EEEE")
return formatter
}()
}

extension Calendar {
func isDateInLastWeek(_ date: Date) -> Bool {
guard let dateBefore7days = self.date(byAdding: .day, value: -7, to: Date()) else {
return false
}
return date > dateBefore7days
}
}
30 changes: 21 additions & 9 deletions StreamChatSwiftUI.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@
4F6D83352C0F05040098C298 /* PollCommentsViewModel_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6D83342C0F05040098C298 /* PollCommentsViewModel_Tests.swift */; };
4F6D83512C1079A00098C298 /* AlertBannerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6D83502C1079A00098C298 /* AlertBannerViewModifier.swift */; };
4F6D83542C1094220098C298 /* AlertBannerViewModifier_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6D83532C1094220098C298 /* AlertBannerViewModifier_Tests.swift */; };
4F7613792DDCB2C900F996E3 /* MessageRelativeDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7613782DDCB2AD00F996E3 /* MessageRelativeDateFormatter.swift */; };
4F7720AE2C58C45200BAEC02 /* OnLoadViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7720AD2C58C45000BAEC02 /* OnLoadViewModifier.swift */; };
4F7DD9A02BFC7C6100599AA6 /* ChatClient+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7DD99F2BFC7C6100599AA6 /* ChatClient+Extensions.swift */; };
4F7DD9A22BFCB2EF00599AA6 /* ChatClientExtensions_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7DD9A12BFCB2EF00599AA6 /* ChatClientExtensions_Tests.swift */; };
4F889C562D7F000700A7BDAF /* ChatMessageExtensions_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F889C552D7F000700A7BDAF /* ChatMessageExtensions_Tests.swift */; };
4F8D64402DDDCF9300026C09 /* MessageRelativeDateFormatter_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F8D643F2DDDCF9300026C09 /* MessageRelativeDateFormatter_Tests.swift */; };
4F9173FD2DDDFFE8003C30B5 /* ChannelListConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F9173FC2DDDFFE3003C30B5 /* ChannelListConfig.swift */; };
4FA3741A2D799CA400294721 /* AppConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA374192D799CA400294721 /* AppConfigurationView.swift */; };
4FA3741D2D799FC300294721 /* AppConfigurationTranslationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA3741C2D799FC300294721 /* AppConfigurationTranslationView.swift */; };
4FA3741F2D79A64F00294721 /* AppConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA3741E2D79A64900294721 /* AppConfiguration.swift */; };
Expand Down Expand Up @@ -612,10 +615,13 @@
4F6D83342C0F05040098C298 /* PollCommentsViewModel_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollCommentsViewModel_Tests.swift; sourceTree = "<group>"; };
4F6D83502C1079A00098C298 /* AlertBannerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertBannerViewModifier.swift; sourceTree = "<group>"; };
4F6D83532C1094220098C298 /* AlertBannerViewModifier_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertBannerViewModifier_Tests.swift; sourceTree = "<group>"; };
4F7613782DDCB2AD00F996E3 /* MessageRelativeDateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRelativeDateFormatter.swift; sourceTree = "<group>"; };
4F7720AD2C58C45000BAEC02 /* OnLoadViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnLoadViewModifier.swift; sourceTree = "<group>"; };
4F7DD99F2BFC7C6100599AA6 /* ChatClient+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatClient+Extensions.swift"; sourceTree = "<group>"; };
4F7DD9A12BFCB2EF00599AA6 /* ChatClientExtensions_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatClientExtensions_Tests.swift; sourceTree = "<group>"; };
4F889C552D7F000700A7BDAF /* ChatMessageExtensions_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageExtensions_Tests.swift; sourceTree = "<group>"; };
4F8D643F2DDDCF9300026C09 /* MessageRelativeDateFormatter_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRelativeDateFormatter_Tests.swift; sourceTree = "<group>"; };
4F9173FC2DDDFFE3003C30B5 /* ChannelListConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelListConfig.swift; sourceTree = "<group>"; };
4FA374192D799CA400294721 /* AppConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationView.swift; sourceTree = "<group>"; };
4FA3741C2D799FC300294721 /* AppConfigurationTranslationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationTranslationView.swift; sourceTree = "<group>"; };
4FA3741E2D79A64900294721 /* AppConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfiguration.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1935,6 +1941,7 @@
8465FD3D2746A95600AF091E /* ImageCDN.swift */,
8465FD482746A95600AF091E /* ImageMerger.swift */,
8465FD422746A95600AF091E /* InputTextView.swift */,
4F7613782DDCB2AD00F996E3 /* MessageRelativeDateFormatter.swift */,
8465FD432746A95600AF091E /* NSLayoutConstraint+Extensions.swift */,
8465FD492746A95600AF091E /* NukeImageProcessor.swift */,
4F7720AD2C58C45000BAEC02 /* OnLoadViewModifier.swift */,
Expand All @@ -1951,22 +1958,23 @@
8465FD4C2746A95600AF091E /* ChatChannelList */ = {
isa = PBXGroup;
children = (
8465FD4D2746A95600AF091E /* ChannelAvatarsMerger.swift */,
8465FD572746A95700AF091E /* ChannelHeaderLoader.swift */,
4F9173FC2DDDFFE3003C30B5 /* ChannelListConfig.swift */,
8465FD4E2746A95600AF091E /* ChatChannelHelperViews.swift */,
8465FD512746A95600AF091E /* ChatChannelList.swift */,
8465FD542746A95700AF091E /* ChatChannelListHeader.swift */,
8465FD592746A95700AF091E /* ChatChannelListItem.swift */,
8465FD552746A95700AF091E /* ChatChannelListScreen.swift */,
8465FD5C2746A95700AF091E /* ChatChannelListView.swift */,
8465FD512746A95600AF091E /* ChatChannelList.swift */,
8465FD582746A95700AF091E /* ChatChannelListViewModel.swift */,
8465FD542746A95700AF091E /* ChatChannelListHeader.swift */,
8465FD5A2746A95700AF091E /* ChatChannelSwipeableListItem.swift */,
8465FD532746A95600AF091E /* ChatChannelNavigatableListItem.swift */,
8465FD592746A95700AF091E /* ChatChannelListItem.swift */,
8465FD4D2746A95600AF091E /* ChannelAvatarsMerger.swift */,
8465FD4E2746A95600AF091E /* ChatChannelHelperViews.swift */,
8465FD5A2746A95700AF091E /* ChatChannelSwipeableListItem.swift */,
8465FD502746A95600AF091E /* DefaultChannelActions.swift */,
8465FD522746A95600AF091E /* NoChannelsView.swift */,
8465FD572746A95700AF091E /* ChannelHeaderLoader.swift */,
91B763A3283EB19800B458A9 /* MoreChannelActionsFullScreenWrappingView.swift */,
8465FD4F2746A95600AF091E /* MoreChannelActionsView.swift */,
8465FD5B2746A95700AF091E /* MoreChannelActionsViewModel.swift */,
91B763A3283EB19800B458A9 /* MoreChannelActionsFullScreenWrappingView.swift */,
8465FD522746A95600AF091E /* NoChannelsView.swift */,
8421BCEF27A44EAE000F977D /* SearchResultsView.swift */,
);
path = ChatChannelList;
Expand Down Expand Up @@ -2186,6 +2194,7 @@
91B79FD8284E7E9C005B6E4F /* ChatUserNamer_Tests.swift */,
84C94D53275A1380007FE2B9 /* DateUtils_Tests.swift */,
84C94D5D275A3AA9007FE2B9 /* ImageCDN_Tests.swift */,
4F8D643F2DDDCF9300026C09 /* MessageRelativeDateFormatter_Tests.swift */,
849988AF2AE6BE4800CC95C9 /* PaddingsConfig_Tests.swift */,
84779C762AEBCA6E000A6A68 /* ReactionsIconProvider_Tests.swift */,
84E1D8272976CCAF00060491 /* SortReactions_Tests.swift */,
Expand Down Expand Up @@ -2793,6 +2802,7 @@
82D64C082AD7E5B700C5C79E /* Operation.swift in Sources */,
82D64BEB2AD7E5B700C5C79E /* ImagePipelineTask.swift in Sources */,
AD2DDA612CB040EA0040B8D4 /* NoThreadsView.swift in Sources */,
4F9173FD2DDDFFE8003C30B5 /* ChannelListConfig.swift in Sources */,
82D64BF92AD7E5B700C5C79E /* ImageProcessors+Resize.swift in Sources */,
8465FDC22746A95700AF091E /* ChatChannelNavigatableListItem.swift in Sources */,
8465FDAD2746A95700AF091E /* ImageCDN.swift in Sources */,
Expand All @@ -2806,6 +2816,7 @@
8465FDA52746A95700AF091E /* Modifiers.swift in Sources */,
8465FDBB2746A95700AF091E /* LoadingView.swift in Sources */,
84D6E4F62B2CA4E300D0056C /* RecordingTipView.swift in Sources */,
4F7613792DDCB2C900F996E3 /* MessageRelativeDateFormatter.swift in Sources */,
846608E3278C303800D3D7B3 /* TypingIndicatorView.swift in Sources */,
84A1CACF2816BCF00046595A /* AddUsersView.swift in Sources */,
82D64BF02AD7E5B700C5C79E /* DataLoading.swift in Sources */,
Expand Down Expand Up @@ -3065,6 +3076,7 @@
84B2B5CA281947E100479CEE /* ViewFrameUtils.swift in Sources */,
8423C342277CBA280092DCF1 /* TypingSuggester_Tests.swift in Sources */,
84507C9A281ACCD70081DDC2 /* AddUsersView_Tests.swift in Sources */,
4F8D64402DDDCF9300026C09 /* MessageRelativeDateFormatter_Tests.swift in Sources */,
84C94D0627578BF2007FE2B9 /* UnwrapAsync.swift in Sources */,
84D6B55A27DF6EC7009C6D07 /* LoadingView_Tests.swift in Sources */,
84C94D0427578BF2007FE2B9 /* TestError.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//
// Copyright © 2025 Stream.io Inc. All rights reserved.
//

import Foundation
@testable import StreamChat
@testable import StreamChatSwiftUI
import XCTest

final class MessageRelativeDateFormatter_Tests: StreamChatTestCase {
private var formatter: MessageRelativeDateFormatter!

override func setUp() {
super.setUp()
formatter = MessageRelativeDateFormatter()
formatter.locale = Locale(identifier: "en_UK")
formatter.todayFormatter.locale = Locale(identifier: "en_UK")
formatter.yesterdayFormatter.locale = Locale(identifier: "en_UK")
}

override func tearDown() {
super.tearDown()
formatter = nil
}

func test_showingTimeOnly() throws {
let date = try XCTUnwrap(Calendar.current.date(bySettingHour: 1, minute: 2, second: 3, of: Date()))
let result = formatter.string(from: date)
let expected = formatter.todayFormatter.string(from: date)
XCTAssertEqual(expected, result)
XCTAssertEqual("01:02", result)
}

func test_showingYesterday() throws {
let date = try XCTUnwrap(Calendar.current.date(byAdding: .day, value: -1, to: Date()))
let result = formatter.string(from: date)
let expected = formatter.yesterdayFormatter.string(from: date)
XCTAssertEqual(expected, result)
XCTAssertEqual("Yesterday", result)
}

func test_showingWeekday() throws {
let date = try XCTUnwrap(Calendar.current.date(byAdding: .day, value: -6, to: Date()))
let result = formatter.string(from: date)
let expected = formatter.weekdayFormatter.string(from: date)
XCTAssertEqual(expected, result)
}

func test_showingShortDate() throws {
let components = DateComponents(
timeZone: TimeZone(secondsFromGMT: 0),
year: 2025,
month: 1,
day: 15,
hour: 3,
minute: 4,
second: 5
)
let date = try XCTUnwrap(Calendar.current.date(from: components))
let result = formatter.string(from: date)
XCTAssertEqual("15/01/2025", result)
}
}
Loading