Skip to content
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### 🐞 Fixed
- Fix querying threads by disabled channels crashing [#3813](https://github.com/GetStream/stream-chat-swift/pull/3813)

## StreamChatUI
### 🔄 Changed
- Change gallery header view to show message timestamp instead of online status [#3818](https://github.com/GetStream/stream-chat-swift/pull/3818)

# [4.88.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.88.0)
_September 09, 2025_

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ public extension Appearance {
/// A formatter that provides a name for a recording based on its position in a list of recordings.
public var audioRecordingNameFormatter: AudioRecordingNameFormatter = DefaultAudioRecordingNameFormatter()

/// A formatter that converts the message timestamp to textual representation for the gallery header view.
public var galleryHeaderViewDateFormatter: GalleryHeaderViewDateFormatter = DefaultGalleryHeaderViewDateFormatter()

/// A boolean value that determines whether Markdown is active for messages to be formatted.
public var isMarkdownEnabled = true
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// Copyright © 2025 Stream.io Inc. All rights reserved.
//

import Foundation

/// A formatter that converts the message timestamp to textual representation for the gallery header view.
public protocol GalleryHeaderViewDateFormatter {
func format(_ date: Date) -> String
}

/// The default gallery header view date formatter.
open class DefaultGalleryHeaderViewDateFormatter: GalleryHeaderViewDateFormatter {
public var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = .autoupdatingCurrent
formatter.dateStyle = .short
formatter.timeStyle = .none
return formatter
}()

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

var calendar: StreamCalendar = Calendar.current

public init() {}

open func format(_ date: Date) -> String {
if calendar.isDateInToday(date) {
return dayFormatter.string(from: date)
}

if calendar.isDateInYesterday(date) {
return dayFormatter.string(from: date)
}

return dateFormatter.string(from: date)
}
}
23 changes: 16 additions & 7 deletions Sources/StreamChatUI/Gallery/GalleryVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ open class GalleryVC: _ViewController,
/// Returns the date formatter function used to represent when the user was last seen online.
open var lastSeenDateFormatter: (Date) -> String? { appearance.formatters.userLastActivity.format }

/// A formatter that converts the message timestamp to textual representation for the gallery header view.
open var messageTimestampFormatter: (Date) -> String? { appearance.formatters.galleryHeaderViewDateFormatter.format }

/// A boolean value indicating if the subtitle of the header should show the message timestamp.
public var showMessageTimestamp: Bool = true

/// Controller for handling the transition for dismissal
open var transitionController: ZoomTransitionController!

Expand Down Expand Up @@ -267,15 +273,18 @@ open class GalleryVC: _ViewController,
override open func updateContent() {
super.updateContent()

if content.message.author.isOnline {
dateLabel.text = L10n.Message.Title.online
if showMessageTimestamp {
dateLabel.text = messageTimestampFormatter(content.message.createdAt)
} else {
if
let lastActive = content.message.author.lastActiveAt,
let timeAgo = lastSeenDateFormatter(lastActive) {
dateLabel.text = timeAgo
if content.message.author.isOnline {
dateLabel.text = L10n.Message.Title.online
} else {
dateLabel.text = L10n.Message.Title.offline
if let lastActive = content.message.author.lastActiveAt,
let timeAgo = lastSeenDateFormatter(lastActive) {
dateLabel.text = timeAgo
} else {
dateLabel.text = L10n.Message.Title.offline
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions StreamChat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1456,6 +1456,8 @@
AD3D0CC026A8727800A6D813 /* SlackChatChannelHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3D0CBF26A8727800A6D813 /* SlackChatChannelHeaderView.swift */; };
AD3D0CC226A88E5100A6D813 /* MessengerChatChannelHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3D0CC126A88E5100A6D813 /* MessengerChatChannelHeaderView.swift */; };
AD3D0CC426A89E6300A6D813 /* iMessageChatChannelHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3D0CC326A89E6300A6D813 /* iMessageChatChannelHeaderView.swift */; };
AD3DB8312E7C48BF0023D377 /* GalleryHeaderViewDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3DB8302E7C48BF0023D377 /* GalleryHeaderViewDateFormatter.swift */; };
AD3DB8322E7C48BF0023D377 /* GalleryHeaderViewDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3DB8302E7C48BF0023D377 /* GalleryHeaderViewDateFormatter.swift */; };
AD3EE5442832921400ACEFD9 /* VirtualTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3D15D8527E9D4B5006B34D7 /* VirtualTime.swift */; };
AD4118832D5E1368000EF88E /* UILabel+highlightText.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD4118822D5E135D000EF88E /* UILabel+highlightText.swift */; };
AD4118842D5E1368000EF88E /* UILabel+highlightText.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD4118822D5E135D000EF88E /* UILabel+highlightText.swift */; };
Expand Down Expand Up @@ -4318,6 +4320,7 @@
AD3D0CBF26A8727800A6D813 /* SlackChatChannelHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlackChatChannelHeaderView.swift; sourceTree = "<group>"; };
AD3D0CC126A88E5100A6D813 /* MessengerChatChannelHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessengerChatChannelHeaderView.swift; sourceTree = "<group>"; };
AD3D0CC326A89E6300A6D813 /* iMessageChatChannelHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iMessageChatChannelHeaderView.swift; sourceTree = "<group>"; };
AD3DB8302E7C48BF0023D377 /* GalleryHeaderViewDateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryHeaderViewDateFormatter.swift; sourceTree = "<group>"; };
AD4118822D5E135D000EF88E /* UILabel+highlightText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+highlightText.swift"; sourceTree = "<group>"; };
AD43DE6C2A712B0F0040C0FD /* ChatChannelListSearchVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatChannelListSearchVC.swift; sourceTree = "<group>"; };
AD43F90826153BAD00F2D4BB /* QuotedChatMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotedChatMessageView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -8949,6 +8952,7 @@
AD99C901279B06E9009DD9C5 /* Appearance+Formatters */ = {
isa = PBXGroup;
children = (
AD3DB8302E7C48BF0023D377 /* GalleryHeaderViewDateFormatter.swift */,
ADC4AAAF2788C8850004BB35 /* Appearance+Formatters.swift */,
40D396232A0905560020DDC9 /* AudioPlaybackRateFormatter.swift */,
40D396242A0905560020DDC9 /* AudioRecordingNameFormatter.swift */,
Expand Down Expand Up @@ -10931,6 +10935,7 @@
AD81AF0525ED141800F17F8F /* CellSeparatorView.swift in Sources */,
790882FD25486BFD00896F03 /* ChatChannelListCollectionViewCell.swift in Sources */,
88CABC4525933EE70061BB67 /* ChatMessageReactionsView.swift in Sources */,
AD3DB8312E7C48BF0023D377 /* GalleryHeaderViewDateFormatter.swift in Sources */,
ADD2A99028FF0CD300A83305 /* ImageSizeCalculator.swift in Sources */,
AD7EFDAA2C78C0AF00625FC5 /* PollCommentListItemCell.swift in Sources */,
E7166CE225BEE20600B03B07 /* Appearance+Images.swift in Sources */,
Expand Down Expand Up @@ -13077,6 +13082,7 @@
C121EBA82746A1E800023E4C /* ChatChannelAvatarView.swift in Sources */,
C121EBA92746A1E800023E4C /* ChatChannelAvatarView+SwiftUI.swift in Sources */,
C121EBAA2746A1E800023E4C /* ChatUserAvatarView.swift in Sources */,
AD3DB8322E7C48BF0023D377 /* GalleryHeaderViewDateFormatter.swift in Sources */,
40824D0F2A1270CB003B61FD /* ChatMessageVoiceRecordingAttachmentListView.swift in Sources */,
C121EBAB2746A1E800023E4C /* CurrentChatUserAvatarView.swift in Sources */,
C121EBAC2746A1E800023E4C /* InputChatMessageView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ final class GalleryVC_Tests: XCTestCase {
)

vc = makeGalleryVC(content: content)
vc.showMessageTimestamp = false
}

override func tearDown() {
Expand Down Expand Up @@ -125,10 +126,43 @@ final class GalleryVC_Tests: XCTestCase {
let vc = TestView()
vc.components = .mock
vc.content = content
vc.showMessageTimestamp = false

AssertSnapshot(vc)
}

func test_snapshotWithMessageTimestampToday() {
let today = Date()
let message = makeMessage(with: [
ChatMessageImageAttachment.mock(
id: .unique,
imageURL: TestImages.yoda.url
).asAnyAttachment
], createdAt: today)

let content = GalleryVC.Content(message: message, currentPage: 0)
let vc = makeGalleryVC(content: content)
vc.showMessageTimestamp = true

AssertSnapshot(vc, variants: [.defaultLight])
}

func test_snapshotWithMessageTimestampOlderDate() {
let olderDate = Date(timeIntervalSince1970: 1_577_836_800)
let message = makeMessage(with: [
ChatMessageImageAttachment.mock(
id: .unique,
imageURL: TestImages.yoda.url
).asAnyAttachment
], createdAt: olderDate)

let content = GalleryVC.Content(message: message, currentPage: 0)
let vc = makeGalleryVC(content: content)
vc.showMessageTimestamp = true

AssertSnapshot(vc, variants: [.defaultLight])
}

private func makeGalleryVC(content: GalleryVC.Content, components: Components = .mock) -> GalleryVC {
let vc = GalleryVC()
vc.components = components
Expand All @@ -137,7 +171,7 @@ final class GalleryVC_Tests: XCTestCase {
return vc
}

private func makeMessage(with attachments: [AnyChatMessageAttachment]) -> ChatMessage {
private func makeMessage(with attachments: [AnyChatMessageAttachment], createdAt: Date = Date(timeIntervalSinceReferenceDate: 0)) -> ChatMessage {
.mock(
id: .unique,
cid: .unique,
Expand All @@ -146,7 +180,7 @@ final class GalleryVC_Tests: XCTestCase {
id: .unique,
name: "Author"
),
createdAt: Date(timeIntervalSinceReferenceDate: 0),
createdAt: createdAt,
attachments: attachments
)
}
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