mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 20:09:04 +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 enableKeyShareOnInvite: Bool
|
||||||
|
|
||||||
|
var floatingTimelineDateEnabled: Bool
|
||||||
|
|
||||||
var bindings: TimelineViewStateBindings
|
var bindings: TimelineViewStateBindings
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TimelineViewStateBindings {
|
struct TimelineViewStateBindings {
|
||||||
var isScrolledToBottom = true
|
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.
|
/// The state of wether reactions listed on the timeline are expanded/collapsed.
|
||||||
/// Key is itemID, value is the collapsed state.
|
/// Key is itemID, value is the collapsed state.
|
||||||
var reactionsCollapsed: [TimelineItemIdentifier: Bool]
|
var reactionsCollapsed: [TimelineItemIdentifier: Bool]
|
||||||
|
|||||||
@@ -145,6 +145,10 @@ class TimelineTableViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Binding private var isScrolledToBottom: Bool
|
@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] {
|
private var timelineItemsIDs: [TimelineItemIdentifier.UniqueID] {
|
||||||
timelineItemsDictionary.keys.elements.reversed()
|
timelineItemsDictionary.keys.elements.reversed()
|
||||||
@@ -175,9 +179,11 @@ class TimelineTableViewController: UIViewController {
|
|||||||
|
|
||||||
init(coordinator: TimelineViewRepresentable.Coordinator,
|
init(coordinator: TimelineViewRepresentable.Coordinator,
|
||||||
isScrolledToBottom: Binding<Bool>,
|
isScrolledToBottom: Binding<Bool>,
|
||||||
|
floatingDateText: Binding<String?>,
|
||||||
scrollToBottomPublisher: PassthroughSubject<Void, Never>) {
|
scrollToBottomPublisher: PassthroughSubject<Void, Never>) {
|
||||||
self.coordinator = coordinator
|
self.coordinator = coordinator
|
||||||
_isScrolledToBottom = isScrolledToBottom
|
_isScrolledToBottom = isScrolledToBottom
|
||||||
|
_floatingDateText = floatingDateText
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
@@ -461,6 +467,10 @@ extension TimelineTableViewController: UITableViewDelegate {
|
|||||||
if self.isScrolledToBottom != isScrolledToBottom {
|
if self.isScrolledToBottom != isScrolledToBottom {
|
||||||
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
|
// 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
|
// MARK: - Layout
|
||||||
|
|
||||||
extension TimelineTableViewController {
|
extension TimelineTableViewController {
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
linkMetadataProvider: hideTimelineMedia ? nil : linkMetadataProvider,
|
linkMetadataProvider: hideTimelineMedia ? nil : linkMetadataProvider,
|
||||||
mapTilerConfiguration: appSettings.mapTilerConfiguration,
|
mapTilerConfiguration: appSettings.mapTilerConfiguration,
|
||||||
enableKeyShareOnInvite: appSettings.enableKeyShareOnInvite,
|
enableKeyShareOnInvite: appSettings.enableKeyShareOnInvite,
|
||||||
|
floatingTimelineDateEnabled: appSettings.floatingTimelineDateEnabled,
|
||||||
bindings: .init(reactionsCollapsed: [:])),
|
bindings: .init(reactionsCollapsed: [:])),
|
||||||
mediaProvider: userSession.mediaProvider)
|
mediaProvider: userSession.mediaProvider)
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ final class RoomTimelineItemViewState: Identifiable, Equatable, ObservableObject
|
|||||||
var id: TimelineItemIdentifier.UniqueID {
|
var id: TimelineItemIdentifier.UniqueID {
|
||||||
identifier.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 {
|
enum RoomTimelineItemType: Equatable {
|
||||||
@@ -147,4 +152,55 @@ enum RoomTimelineItemType: Equatable {
|
|||||||
return item.id
|
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