Skip to content

Change timestamp formatting according to the default design #2736

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
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 @@ -3,7 +3,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

# Upcoming

## StreamChatUI
### 🔄 Changed
- Change timestamp formatting in Channel List according to the default design and other SDKs [#2736](https://github.com/GetStream/stream-chat-swift/pull/2736)

# [4.35.2](https://github.com/GetStream/stream-chat-swift/releases/tag/4.35.2)
_August 16, 2023_
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import StreamChat

public extension Appearance {
struct Formatters {
/// A formatter that converts the message date separator used in the message list to textual representation.
/// A formatter that converts the message to textual representation in the message list.
public var messageTimestamp: MessageTimestampFormatter = DefaultMessageTimestampFormatter()

/// A formatter that converts the message to textual representation in the channel list.
public var channelListMessageTimestamp: MessageTimestampFormatter = ChannelListMessageTimestampFormatter()

/// A formatter that converts the message date separator to textual representation.
/// This formatter is used to display the message date between each group of messages
/// and the top date overlay in the message list.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//
// Copyright © 2023 Stream.io Inc. All rights reserved.
//

import Foundation

/// The last message timestamp formatter in channel list.
open class ChannelListMessageTimestampFormatter: MessageTimestampFormatter {
/// The formatter to show the time that a message was sent if it was sent today.
public var timeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .short
formatter.locale = .autoupdatingCurrent
return formatter
}()

/// The formatter to show "Yesterday" in case the message was sent yesterday.
public var relativeDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .none
formatter.doesRelativeDateFormatting = true
formatter.locale = .autoupdatingCurrent
return formatter
}()

/// The formatter to show the week day that a message was sent.
public var weekDayDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.setLocalizedDateFormatFromTemplate("EEEE")
formatter.locale = .autoupdatingCurrent
return formatter
}()

/// The formatter to show the date that a message was sent in case it was sent before the last week.
public var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .none
formatter.locale = .autoupdatingCurrent
return formatter
}()

var calendar: ChannelListMessageTimestampCalendar = Calendar.current

public init() {}

open func format(_ date: Date) -> String {
if calendar.isDateInToday(date) {
return timeFormatter.string(from: date)
}
if calendar.isDateInYesterday(date) {
return relativeDateFormatter.string(from: date)
}
if calendar.isDateInLastWeek(date) {
return weekDayDateFormatter.string(from: date)
}

return dateFormatter.string(from: date)
}
}

protocol ChannelListMessageTimestampCalendar {
func isDateInToday(_ date: Date) -> Bool
func isDateInYesterday(_ date: Date) -> Bool
func isDateInLastWeek(_ date: Date) -> Bool
}

extension Calendar: ChannelListMessageTimestampCalendar {
func isDateInLastWeek(_ date: Date) -> Bool {
guard let dateBefore7days = self.date(byAdding: .day, value: -7, to: Date()) else {
return false
}

return date > dateBefore7days
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ open class ChatChannelListItemView: _View, ThemeProvider, SwiftUIRepresentable {
}

/// A formatter that converts the message timestamp to textual representation.
public lazy var timestampFormatter: MessageTimestampFormatter = appearance.formatters.messageTimestamp
public lazy var timestampFormatter: MessageTimestampFormatter = appearance.formatters.channelListMessageTimestamp

/// Main container which holds `avatarView` and two horizontal containers `title` and `unreadCount` and
/// `subtitle` and `timestampLabel`
Expand Down
6 changes: 6 additions & 0 deletions StreamChat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1332,6 +1332,8 @@
ADD2A99828FF227D00A83305 /* ImageSizeCalculator_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD2A99528FF227800A83305 /* ImageSizeCalculator_Tests.swift */; };
ADD2A99A28FF4F4B00A83305 /* StreamCDN.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD2A99928FF4F4B00A83305 /* StreamCDN.swift */; };
ADD2A99B28FF4F4B00A83305 /* StreamCDN.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD2A99928FF4F4B00A83305 /* StreamCDN.swift */; };
ADD738472A8D312B0011FE81 /* ChannelListMessageTimestampFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD738462A8D312B0011FE81 /* ChannelListMessageTimestampFormatter.swift */; };
ADD738482A8D312B0011FE81 /* ChannelListMessageTimestampFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD738462A8D312B0011FE81 /* ChannelListMessageTimestampFormatter.swift */; };
ADDB2F592954CBF500BF80DA /* ViewPaginationHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADDB2F572954CBA200BF80DA /* ViewPaginationHandling.swift */; };
ADDB2F5A2954CBF700BF80DA /* ViewPaginationHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADDB2F572954CBA200BF80DA /* ViewPaginationHandling.swift */; };
ADDB2F5F2954D43D00BF80DA /* ScrollViewPaginationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADDB2F5B2954CC0A00BF80DA /* ScrollViewPaginationHandler.swift */; };
Expand Down Expand Up @@ -3712,6 +3714,7 @@
ADD2A99528FF227800A83305 /* ImageSizeCalculator_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSizeCalculator_Tests.swift; sourceTree = "<group>"; };
ADD2A99928FF4F4B00A83305 /* StreamCDN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamCDN.swift; sourceTree = "<group>"; };
ADD5A9E725DE8AF6006DC88A /* ChatSuggestionsVC_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatSuggestionsVC_Tests.swift; sourceTree = "<group>"; };
ADD738462A8D312B0011FE81 /* ChannelListMessageTimestampFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelListMessageTimestampFormatter.swift; sourceTree = "<group>"; };
ADDB2F572954CBA200BF80DA /* ViewPaginationHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewPaginationHandling.swift; sourceTree = "<group>"; };
ADDB2F5B2954CC0A00BF80DA /* ScrollViewPaginationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewPaginationHandler.swift; sourceTree = "<group>"; };
ADDB2F5D2954CC1700BF80DA /* InvertedScrollViewPaginationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvertedScrollViewPaginationHandler.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -7701,6 +7704,7 @@
ADBBDA27279F0E9B00E47B1C /* ChannelNameFormatter.swift */,
AD99C906279B0C9B009DD9C5 /* MessageDateSeparatorFormatter.swift */,
AD99C902279B0716009DD9C5 /* MessageTimestampFormatter.swift */,
ADD738462A8D312B0011FE81 /* ChannelListMessageTimestampFormatter.swift */,
ADBBDA21279F0CFA00E47B1C /* UploadingProgressFormatter.swift */,
AD99C90A279B1363009DD9C5 /* UserLastActivityFormatter.swift */,
ADBBDA1E279F0CEA00E47B1C /* VideoDurationFormatter.swift */,
Expand Down Expand Up @@ -9397,6 +9401,7 @@
A3BB3FFF261DA74D00365496 /* ContainerStackView.swift in Sources */,
ADBBDA22279F0CFA00E47B1C /* UploadingProgressFormatter.swift in Sources */,
8830518E263038190069D731 /* ChatReactionsBubbleView.swift in Sources */,
ADD738472A8D312B0011FE81 /* ChannelListMessageTimestampFormatter.swift in Sources */,
AD793F4B270B769E00B05456 /* ChatMessageReactionAuthorViewCell.swift in Sources */,
F87A4956260C6F38001653A8 /* ChatMessageContentView+SwiftUI.swift in Sources */,
88CABC4325933EE70061BB67 /* ChatMessageReactionsPickerVC.swift in Sources */,
Expand Down Expand Up @@ -11301,6 +11306,7 @@
AD78F9FB28EC735700BC0FCE /* SwiftyLineProcessor.swift in Sources */,
C1788F6029C33A1000149883 /* ChatThreadRepliesCountDecorationView.swift in Sources */,
40824D0A2A1270BF003B61FD /* VoiceRecordingAttachmentViewInjector.swift in Sources */,
ADD738482A8D312B0011FE81 /* ChannelListMessageTimestampFormatter.swift in Sources */,
C121EBD72746A1EA00023E4C /* ChatMessageReactions+Types.swift in Sources */,
AD78F9F928EC735700BC0FCE /* CharacterRule.swift in Sources */,
CF38F5B0287DB53E00E24D10 /* ChatChannelListErrorView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ final class ChatChannelListItemView_Tests: XCTestCase {

view.addSizeConstraints()
view.components = .mock
view.appearance.formatters.channelListMessageTimestamp = DefaultMessageTimestampFormatter()

view.content = .init(
channel: channel(
Expand Down Expand Up @@ -724,7 +725,7 @@ final class ChatChannelListItemView_Tests: XCTestCase {

// MARK: - Timestamp

func test_timestampText_isNil_whenPreviewMessageIsNil() {
func test_timestampText_whenPreviewMessageIsNil_thenTimestampIsNil() {
let channel: ChatChannel = .mock(
cid: .unique,
previewMessage: nil
Expand All @@ -735,28 +736,25 @@ final class ChatChannelListItemView_Tests: XCTestCase {
XCTAssertNil(itemView.timestampText)
}

func test_timestampText_whenPreviewMessageExists() {
func test_timestampText_whenPreviewMessageExists_thenUsesCreatedAtFromPreviewMessage() {
let channel: ChatChannel = .mock(
cid: .unique,
previewMessage: .mock(
id: .unique,
cid: .unique,
text: .unique,
author: .mock(id: .unique),
createdAt: Date(timeIntervalSince1970: 1)
)
)

let itemView = ChatChannelListItemView()
itemView.content = .init(channel: channel, currentUserId: nil)
itemView.appearance.formatters.channelListMessageTimestamp = DefaultMessageTimestampFormatter()

XCTAssertEqual(
itemView.timestampText,
"12:00 AM"
)
}

func test_timestampText_whenSearchingMessage() {
func test_timestampText_whenSearchingMessage_thenUsesCreatedAtFromSearchResultMessage() {
let itemView = ChatChannelListItemView()
itemView.content = .init(
channel: .mockNonDMChannel(previewMessage: nil),
Expand All @@ -766,13 +764,94 @@ final class ChatChannelListItemView_Tests: XCTestCase {
message: .mock(text: "Some text", createdAt: Date(timeIntervalSince1970: 1))
)
)
itemView.appearance.formatters.channelListMessageTimestamp = DefaultMessageTimestampFormatter()

XCTAssertEqual(
itemView.timestampText,
"12:00 AM"
)
}

func test_timestampText_whenCreatedAtIsToday_thenShowsTimeOnly() {
let channel: ChatChannel = .mock(
cid: .unique,
previewMessage: .mock(
createdAt: Date(timeIntervalSince1970: 1)
)
)

let mockCalendar = Calendar_Mock()
mockCalendar.mockIsDateInToday = true
let formatter = ChannelListMessageTimestampFormatter()
formatter.calendar = mockCalendar

let itemView = ChatChannelListItemView()
itemView.content = .init(channel: channel, currentUserId: nil)
itemView.appearance.formatters.channelListMessageTimestamp = formatter

XCTAssertEqual(
itemView.timestampText,
"12:00 AM"
)
}

func test_timestampText_whenCreatedAtIsYesterday_thenShowsYesterday() {
let channel: ChatChannel = .mock(
cid: .unique,
previewMessage: .mock(
createdAt: Calendar.current.date(byAdding: .day, value: -1, to: Date()) ?? Date()
)
)

let itemView = ChatChannelListItemView()
itemView.content = .init(channel: channel, currentUserId: nil)

XCTAssertEqual(
itemView.timestampText,
"Yesterday"
)
}

func test_timestampText_whenCreatedAtInLastWeek_thenShowsWeekDay() {
let channel: ChatChannel = .mock(
cid: .unique,
previewMessage: .mock(
createdAt: Date(timeIntervalSince1970: 1_690_998_292)
)
)

let mockCalendar = Calendar_Mock()
mockCalendar.mockIsDateInLastWeek = true
let formatter = ChannelListMessageTimestampFormatter()
formatter.calendar = mockCalendar

let itemView = ChatChannelListItemView()
itemView.content = .init(channel: channel, currentUserId: nil)
itemView.appearance.formatters.channelListMessageTimestamp = formatter

XCTAssertEqual(
itemView.timestampText,
"Wednesday"
)
}

func test_timestampText_whenCreatedAtBeforeLastWeek_thenShowsDate() {
let channel: ChatChannel = .mock(
cid: .unique,
previewMessage: .mock(
createdAt: Date(timeIntervalSince1970: 1_690_998_292)
)
)

let itemView = ChatChannelListItemView()
itemView.content = .init(channel: channel, currentUserId: nil)

XCTAssertEqual(
itemView.timestampText,
"8/2/23"
)
}

// MARK: - Delivery status

func test_previewMessageDeliveryStatus_whenPreviewMessageIsNil() {
Expand Down Expand Up @@ -988,6 +1067,7 @@ final class ChatChannelListItemView_Tests: XCTestCase {
let view = ChatChannelListItemView().withoutAutoresizingMaskConstraints
view.components = components
view.appearance = appearance
view.appearance.formatters.channelListMessageTimestamp = DefaultMessageTimestampFormatter()
view.content = content
view.addSizeConstraints()
return view
Expand Down Expand Up @@ -1015,3 +1095,21 @@ private extension ChatChannelListItemView {
])
}
}

private class Calendar_Mock: ChannelListMessageTimestampCalendar {
var mockIsDateInToday = false
var mockIsDateInYesterday = false
var mockIsDateInLastWeek = false

func isDateInToday(_ date: Date) -> Bool {
mockIsDateInToday
}

func isDateInYesterday(_ date: Date) -> Bool {
mockIsDateInYesterday
}

func isDateInLastWeek(_ date: Date) -> Bool {
mockIsDateInLastWeek
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ final class ChatChannelListView_Tests: iOS13TestCase {

// TODO: We have to replace default as the components are not injected in SwiftUI views.
Components.default = .mock
Appearance.default.formatters.channelListMessageTimestamp = DefaultMessageTimestampFormatter()
mockedChannelListController = ChatChannelListController_Mock.mock()
chatChannelList = ChatChannelListVC.asView(mockedChannelListController)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ final class ChatChannelListVC_Tests: XCTestCase {
var components = Components.mock
components.channelListRouter = ChatChannelListRouterMock.self
vc.components = components
vc.appearance.formatters.channelListMessageTimestamp = DefaultMessageTimestampFormatter()

channels = .dummy()
}
Expand Down Expand Up @@ -121,6 +122,7 @@ final class ChatChannelListVC_Tests: XCTestCase {

let vc = TestView()
vc.controller = mockedChannelListController
vc.appearance.formatters.channelListMessageTimestamp = DefaultMessageTimestampFormatter()

var components = Components.mock
components.channelCellSeparator = TestSeparatorView.self
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ final class ChatChannelSearchVC_Tests: XCTestCase {

vc = ChatChannelSearchVC()
vc.controller = mockedChannelListController
vc.appearance.formatters.channelListMessageTimestamp = DefaultMessageTimestampFormatter()
}

override func tearDown() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ final class ChatMessageSearchVC_Tests: XCTestCase {

vc = ChatMessageSearchVC()
vc.messageSearchController = mockedMessageSearchController
vc.appearance.formatters.channelListMessageTimestamp = DefaultMessageTimestampFormatter()
}

override func tearDown() {
Expand Down