diff --git a/CHANGELOG.md b/CHANGELOG.md index 61c5c19189..44ac1a5445 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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_ diff --git a/Sources/StreamChatUI/Appearance+Formatters/Appearance+Formatters.swift b/Sources/StreamChatUI/Appearance+Formatters/Appearance+Formatters.swift index 889be3567b..d08912a3ff 100644 --- a/Sources/StreamChatUI/Appearance+Formatters/Appearance+Formatters.swift +++ b/Sources/StreamChatUI/Appearance+Formatters/Appearance+Formatters.swift @@ -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 } diff --git a/Sources/StreamChatUI/Appearance+Formatters/GalleryHeaderViewDateFormatter.swift b/Sources/StreamChatUI/Appearance+Formatters/GalleryHeaderViewDateFormatter.swift new file mode 100644 index 0000000000..5391dd751c --- /dev/null +++ b/Sources/StreamChatUI/Appearance+Formatters/GalleryHeaderViewDateFormatter.swift @@ -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) + } +} diff --git a/Sources/StreamChatUI/Gallery/GalleryVC.swift b/Sources/StreamChatUI/Gallery/GalleryVC.swift index ba69b80d49..364b16dfd1 100644 --- a/Sources/StreamChatUI/Gallery/GalleryVC.swift +++ b/Sources/StreamChatUI/Gallery/GalleryVC.swift @@ -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! @@ -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 + } } } diff --git a/StreamChat.xcodeproj/project.pbxproj b/StreamChat.xcodeproj/project.pbxproj index dd0bd7ff39..911ced54a0 100644 --- a/StreamChat.xcodeproj/project.pbxproj +++ b/StreamChat.xcodeproj/project.pbxproj @@ -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 */; }; @@ -4318,6 +4320,7 @@ AD3D0CBF26A8727800A6D813 /* SlackChatChannelHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlackChatChannelHeaderView.swift; sourceTree = ""; }; AD3D0CC126A88E5100A6D813 /* MessengerChatChannelHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessengerChatChannelHeaderView.swift; sourceTree = ""; }; AD3D0CC326A89E6300A6D813 /* iMessageChatChannelHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iMessageChatChannelHeaderView.swift; sourceTree = ""; }; + AD3DB8302E7C48BF0023D377 /* GalleryHeaderViewDateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryHeaderViewDateFormatter.swift; sourceTree = ""; }; AD4118822D5E135D000EF88E /* UILabel+highlightText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+highlightText.swift"; sourceTree = ""; }; AD43DE6C2A712B0F0040C0FD /* ChatChannelListSearchVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatChannelListSearchVC.swift; sourceTree = ""; }; AD43F90826153BAD00F2D4BB /* QuotedChatMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotedChatMessageView.swift; sourceTree = ""; }; @@ -8949,6 +8952,7 @@ AD99C901279B06E9009DD9C5 /* Appearance+Formatters */ = { isa = PBXGroup; children = ( + AD3DB8302E7C48BF0023D377 /* GalleryHeaderViewDateFormatter.swift */, ADC4AAAF2788C8850004BB35 /* Appearance+Formatters.swift */, 40D396232A0905560020DDC9 /* AudioPlaybackRateFormatter.swift */, 40D396242A0905560020DDC9 /* AudioRecordingNameFormatter.swift */, @@ -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 */, @@ -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 */, diff --git a/Tests/StreamChatUITests/SnapshotTests/Gallery/GalleryVC_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/Gallery/GalleryVC_Tests.swift index 9b1220b2c2..ef93b8b13d 100644 --- a/Tests/StreamChatUITests/SnapshotTests/Gallery/GalleryVC_Tests.swift +++ b/Tests/StreamChatUITests/SnapshotTests/Gallery/GalleryVC_Tests.swift @@ -30,6 +30,7 @@ final class GalleryVC_Tests: XCTestCase { ) vc = makeGalleryVC(content: content) + vc.showMessageTimestamp = false } override func tearDown() { @@ -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 @@ -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, @@ -146,7 +180,7 @@ final class GalleryVC_Tests: XCTestCase { id: .unique, name: "Author" ), - createdAt: Date(timeIntervalSinceReferenceDate: 0), + createdAt: createdAt, attachments: attachments ) } diff --git a/Tests/StreamChatUITests/SnapshotTests/Gallery/__Snapshots__/GalleryVC_Tests/test_snapshotWithMessageTimestampOlderDate.default-light.png b/Tests/StreamChatUITests/SnapshotTests/Gallery/__Snapshots__/GalleryVC_Tests/test_snapshotWithMessageTimestampOlderDate.default-light.png new file mode 100644 index 0000000000..a1cb23420e Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/Gallery/__Snapshots__/GalleryVC_Tests/test_snapshotWithMessageTimestampOlderDate.default-light.png differ diff --git a/Tests/StreamChatUITests/SnapshotTests/Gallery/__Snapshots__/GalleryVC_Tests/test_snapshotWithMessageTimestampToday.default-light.png b/Tests/StreamChatUITests/SnapshotTests/Gallery/__Snapshots__/GalleryVC_Tests/test_snapshotWithMessageTimestampToday.default-light.png new file mode 100644 index 0000000000..f9d3b9f57b Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/Gallery/__Snapshots__/GalleryVC_Tests/test_snapshotWithMessageTimestampToday.default-light.png differ