Skip to content

Show dynamically time, relative date, weekday, or short date in channel lists #833

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

Merged
merged 4 commits into from
May 27, 2025
Merged
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
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
- Add `MessageViewModel` to `MessageContainerView` to make it easier to customise presentation logic [#815](https://github.com/GetStream/stream-chat-swiftui/pull/815)
- Add `MessageListConfig.messaeDisplayOptions.showOriginalTranslatedButton` to enable showing original text in translated message [#815](https://github.com/GetStream/stream-chat-swiftui/pull/815)
- Add `Utils.originalTranslationsStore` to keep track of messages that should show the original text [#815](https://github.com/GetStream/stream-chat-swiftui/pull/815)
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(
messageDisplayOptions: .init(showOriginalTranslatedButton: true),
dateIndicatorPlacement: .messageList,
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 @@ -71,6 +77,7 @@ public class Utils {

public init(
dateFormatter: DateFormatter = .makeDefault(),
messageRelativeDateFormatter: DateFormatter = MessageRelativeDateFormatter(),
videoPreviewLoader: VideoPreviewLoader = DefaultVideoPreviewLoader(),
imageLoader: ImageLoading = NukeImageLoader(),
imageCDN: ImageCDN = StreamImageCDN(),
Expand All @@ -81,6 +88,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 @@ -95,6 +103,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 @@ -107,6 +116,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 @@ -614,10 +617,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 @@ -1941,6 +1947,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 @@ -1957,22 +1964,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 @@ -2192,6 +2200,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 @@ -2801,6 +2810,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 @@ -2814,6 +2824,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 @@ -3073,6 +3084,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