mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
implemented the display logic for the formatted date
This commit is contained in:
@@ -142,12 +142,17 @@ struct TimelineViewState: BindableState {
|
||||
|
||||
var enableKeyShareOnInvite: Bool
|
||||
|
||||
var floatingTimelineDateEnabled: Bool
|
||||
|
||||
var bindings: TimelineViewStateBindings
|
||||
}
|
||||
|
||||
struct TimelineViewStateBindings {
|
||||
var isScrolledToBottom = true
|
||||
|
||||
/// The formatted date text for the floating date badge shown while scrolling.
|
||||
var floatingDateText: String?
|
||||
|
||||
/// The state of wether reactions listed on the timeline are expanded/collapsed.
|
||||
/// Key is itemID, value is the collapsed state.
|
||||
var reactionsCollapsed: [TimelineItemIdentifier: Bool]
|
||||
|
||||
@@ -145,6 +145,10 @@ class TimelineTableViewController: UIViewController {
|
||||
}
|
||||
|
||||
@Binding private var isScrolledToBottom: Bool
|
||||
@Binding private var floatingDateText: String?
|
||||
|
||||
/// A work item used to auto-hide the floating date badge after scrolling stops.
|
||||
private var floatingDateHideWorkItem: DispatchWorkItem?
|
||||
|
||||
private var timelineItemsIDs: [TimelineItemIdentifier.UniqueID] {
|
||||
timelineItemsDictionary.keys.elements.reversed()
|
||||
@@ -175,9 +179,11 @@ class TimelineTableViewController: UIViewController {
|
||||
|
||||
init(coordinator: TimelineViewRepresentable.Coordinator,
|
||||
isScrolledToBottom: Binding<Bool>,
|
||||
floatingDateText: Binding<String?>,
|
||||
scrollToBottomPublisher: PassthroughSubject<Void, Never>) {
|
||||
self.coordinator = coordinator
|
||||
_isScrolledToBottom = isScrolledToBottom
|
||||
_floatingDateText = floatingDateText
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
@@ -461,6 +467,10 @@ extension TimelineTableViewController: UITableViewDelegate {
|
||||
if self.isScrolledToBottom != isScrolledToBottom {
|
||||
self.isScrolledToBottom = isScrolledToBottom
|
||||
}
|
||||
|
||||
if !isScrolledToBottom {
|
||||
updateFloatingDate()
|
||||
}
|
||||
}
|
||||
|
||||
// We never want the table view to be fully at the bottom to allow the status bar tap to work properly
|
||||
@@ -505,6 +515,63 @@ extension TimelineTableViewController: UITableViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Floating Date Badge
|
||||
|
||||
extension TimelineTableViewController {
|
||||
/// Computes the formatted date text for the topmost visible timeline item
|
||||
/// and updates the floating date binding.
|
||||
func updateFloatingDate() {
|
||||
guard let dateText = topmostVisibleDateText() else {
|
||||
return
|
||||
}
|
||||
|
||||
// Before updating it already schedule it's removal or the future
|
||||
scheduleFloatingDateHide()
|
||||
|
||||
// Only update on changes to avoid needless SwiftUI recomputation.
|
||||
if floatingDateText != dateText {
|
||||
floatingDateText = dateText
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedules the floating date badge to be hidden after a delay.
|
||||
func scheduleFloatingDateHide() {
|
||||
floatingDateHideWorkItem?.cancel()
|
||||
let workItem = DispatchWorkItem { [weak self] in
|
||||
self?.floatingDateText = nil
|
||||
}
|
||||
floatingDateHideWorkItem = workItem
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: workItem)
|
||||
}
|
||||
|
||||
/// Returns the formatted date text for the topmost visible timeline item.
|
||||
///
|
||||
/// The table view is flipped (unless VoiceOver is running), so the "topmost"
|
||||
/// visible cell on screen is actually the *last* index path in `indexPathsForVisibleRows`
|
||||
/// when the table is flipped, or the *first* when it is not.
|
||||
private func topmostVisibleDateText() -> String? {
|
||||
guard let visibleIndexPaths = tableView.indexPathsForVisibleRows,
|
||||
!visibleIndexPaths.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// In a flipped table view the last index path is the topmost item on screen;
|
||||
// when VoiceOver is active the table is not flipped so the first is topmost.
|
||||
let isFlipped = scaleY == -1
|
||||
let orderedPaths = isFlipped ? visibleIndexPaths.reversed() : visibleIndexPaths
|
||||
|
||||
// Walk from topmost downward and return the date of the first item that has one.
|
||||
for indexPath in orderedPaths {
|
||||
if let uniqueID = dataSource?.itemIdentifier(for: indexPath),
|
||||
let timestamp = timelineItemsDictionary[uniqueID]?.timestamp {
|
||||
return timestamp.formattedDateSeparator()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
extension TimelineTableViewController {
|
||||
|
||||
@@ -108,6 +108,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
linkMetadataProvider: hideTimelineMedia ? nil : linkMetadataProvider,
|
||||
mapTilerConfiguration: appSettings.mapTilerConfiguration,
|
||||
enableKeyShareOnInvite: appSettings.enableKeyShareOnInvite,
|
||||
floatingTimelineDateEnabled: appSettings.floatingTimelineDateEnabled,
|
||||
bindings: .init(reactionsCollapsed: [:])),
|
||||
mediaProvider: userSession.mediaProvider)
|
||||
|
||||
|
||||
@@ -39,6 +39,11 @@ final class RoomTimelineItemViewState: Identifiable, Equatable, ObservableObject
|
||||
var id: TimelineItemIdentifier.UniqueID {
|
||||
identifier.uniqueID
|
||||
}
|
||||
|
||||
/// The timestamp of the item if available (separator date or event timestamp), `nil` otherwise.
|
||||
var timestamp: Date? {
|
||||
type.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
enum RoomTimelineItemType: Equatable {
|
||||
@@ -147,4 +152,55 @@ enum RoomTimelineItemType: Equatable {
|
||||
return item.id
|
||||
}
|
||||
}
|
||||
|
||||
/// The timestamp of the item if available.
|
||||
/// Returns the separator date for `.separator`, the event timestamp for all event-based
|
||||
/// items, the timestamp of the first event inside a `.group`, and `nil` for virtual items
|
||||
/// that carry no timestamp (read marker, pagination indicator, timeline start).
|
||||
var timestamp: Date? {
|
||||
switch self {
|
||||
case .separator(let item):
|
||||
return item.timestamp
|
||||
case .text(let item):
|
||||
return item.timestamp
|
||||
case .image(let item):
|
||||
return item.timestamp
|
||||
case .video(let item):
|
||||
return item.timestamp
|
||||
case .audio(let item):
|
||||
return item.timestamp
|
||||
case .file(let item):
|
||||
return item.timestamp
|
||||
case .emote(let item):
|
||||
return item.timestamp
|
||||
case .notice(let item):
|
||||
return item.timestamp
|
||||
case .redacted(let item):
|
||||
return item.timestamp
|
||||
case .encrypted(let item):
|
||||
return item.timestamp
|
||||
case .sticker(let item):
|
||||
return item.timestamp
|
||||
case .unsupported(let item):
|
||||
return item.timestamp
|
||||
case .state(let item):
|
||||
return item.timestamp
|
||||
case .location(let item):
|
||||
return item.timestamp
|
||||
case .poll(let item):
|
||||
return item.timestamp
|
||||
case .voice(let item):
|
||||
return item.timestamp
|
||||
case .callInvite(let item):
|
||||
return item.timestamp
|
||||
case .callNotification(let item):
|
||||
return item.timestamp
|
||||
case .liveLocation(let item):
|
||||
return item.timestamp
|
||||
case .group(let item):
|
||||
return (item.items.first(where: { $0 is EventBasedTimelineItemProtocol }) as? EventBasedTimelineItemProtocol)?.timestamp
|
||||
case .readMarker, .paginationIndicator, .timelineStart:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user