mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Add an action to the newly introduced FloatingDateBadge that scrolls the timeline to that day. (#5350)
and replace the TimelnieTableViewController's `floatingDateText` with a pure date.
This commit is contained in:
@@ -110,7 +110,9 @@ struct RoomScreen: View {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var dateBadge: some View {
|
private var dateBadge: some View {
|
||||||
if !isVoiceOverEnabled {
|
if !isVoiceOverEnabled {
|
||||||
FloatingDateBadge(dateText: timelineContext.floatingDateText)
|
FloatingDateBadge(dateText: timelineContext.floatingDate?.formattedDateSeparator()) {
|
||||||
|
timelineContext.send(viewAction: .scrollToFirstItemForCurrentDate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,10 @@ struct ThreadTimelineScreen: View {
|
|||||||
.toolbarBackground(.visible, for: .navigationBar) // Fix the toolbar's background.
|
.toolbarBackground(.visible, for: .navigationBar) // Fix the toolbar's background.
|
||||||
.timelineMediaPreview(viewModel: $context.mediaPreviewViewModel)
|
.timelineMediaPreview(viewModel: $context.mediaPreviewViewModel)
|
||||||
.overlay(alignment: .top) {
|
.overlay(alignment: .top) {
|
||||||
FloatingDateBadge(dateText: timelineContext.floatingDateText)
|
FloatingDateBadge(dateText: timelineContext.floatingDate?.formattedDateSeparator()) {
|
||||||
.padding(.top, 13)
|
timelineContext.send(viewAction: .scrollToFirstItemForCurrentDate)
|
||||||
|
}
|
||||||
|
.padding(.top, 13)
|
||||||
}
|
}
|
||||||
.overlay(alignment: .bottomTrailing) {
|
.overlay(alignment: .bottomTrailing) {
|
||||||
TimelineScrollToBottomButton(isVisible: isAtBottomAndLive) {
|
TimelineScrollToBottomButton(isVisible: isAtBottomAndLive) {
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ enum TimelineViewAction {
|
|||||||
case paginateBackwards
|
case paginateBackwards
|
||||||
case paginateForwards
|
case paginateForwards
|
||||||
case scrollToBottom
|
case scrollToBottom
|
||||||
|
case scrollToFirstItemForCurrentDate
|
||||||
|
|
||||||
case displayTimelineItemMenu(itemID: TimelineItemIdentifier)
|
case displayTimelineItemMenu(itemID: TimelineItemIdentifier)
|
||||||
case handleTimelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction)
|
case handleTimelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction)
|
||||||
@@ -152,8 +153,8 @@ struct TimelineViewState: BindableState {
|
|||||||
struct TimelineViewStateBindings {
|
struct TimelineViewStateBindings {
|
||||||
var isScrolledToBottom = true
|
var isScrolledToBottom = true
|
||||||
|
|
||||||
/// The formatted date text for the floating date badge shown while scrolling.
|
/// The timestamp of the topmost visible item, used to drive the floating date badge while scrolling.
|
||||||
var floatingDateText: String?
|
var floatingDate: Date?
|
||||||
|
|
||||||
/// 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.
|
||||||
@@ -247,6 +248,7 @@ struct TimelineState {
|
|||||||
|
|
||||||
/// These can be removed when we have full swiftUI and moved as @State values in the view
|
/// These can be removed when we have full swiftUI and moved as @State values in the view
|
||||||
var scrollToBottomPublisher = PassthroughSubject<Void, Never>()
|
var scrollToBottomPublisher = PassthroughSubject<Void, Never>()
|
||||||
|
var scrollToFirstItemForDatePublisher = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
var itemsDictionary = OrderedDictionary<TimelineItemIdentifier.UniqueID, RoomTimelineItemViewState>()
|
var itemsDictionary = OrderedDictionary<TimelineItemIdentifier.UniqueID, RoomTimelineItemViewState>()
|
||||||
|
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ class TimelineTableViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Binding private var isScrolledToBottom: Bool
|
@Binding private var isScrolledToBottom: Bool
|
||||||
@Binding private var floatingDateText: String?
|
@Binding private var floatingDate: Date?
|
||||||
|
|
||||||
/// A work item used to auto-hide the floating date badge after scrolling stops.
|
/// A work item used to auto-hide the floating date badge after scrolling stops.
|
||||||
private var floatingDateHideWorkItem: DispatchWorkItem?
|
private var floatingDateHideWorkItem: DispatchWorkItem?
|
||||||
@@ -179,11 +179,12 @@ class TimelineTableViewController: UIViewController {
|
|||||||
|
|
||||||
init(coordinator: TimelineViewRepresentable.Coordinator,
|
init(coordinator: TimelineViewRepresentable.Coordinator,
|
||||||
isScrolledToBottom: Binding<Bool>,
|
isScrolledToBottom: Binding<Bool>,
|
||||||
floatingDateText: Binding<String?>,
|
floatingDate: Binding<Date?>,
|
||||||
scrollToBottomPublisher: PassthroughSubject<Void, Never>) {
|
scrollToBottomPublisher: PassthroughSubject<Void, Never>,
|
||||||
|
scrollToFirstItemForDatePublisher: PassthroughSubject<Void, Never>) {
|
||||||
self.coordinator = coordinator
|
self.coordinator = coordinator
|
||||||
_isScrolledToBottom = isScrolledToBottom
|
_isScrolledToBottom = isScrolledToBottom
|
||||||
_floatingDateText = floatingDateText
|
_floatingDate = floatingDate
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
@@ -209,6 +210,12 @@ class TimelineTableViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
scrollToFirstItemForDatePublisher
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
self?.scrollToFirstItemForCurrentDate()
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
paginatePublisher
|
paginatePublisher
|
||||||
.collect(.byTime(DispatchQueue.main, 0.1))
|
.collect(.byTime(DispatchQueue.main, 0.1))
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
@@ -518,10 +525,10 @@ extension TimelineTableViewController: UITableViewDelegate {
|
|||||||
// MARK: - Floating Date Badge
|
// MARK: - Floating Date Badge
|
||||||
|
|
||||||
extension TimelineTableViewController {
|
extension TimelineTableViewController {
|
||||||
/// Computes the formatted date text for the topmost visible timeline item
|
/// Computes the timestamp for the topmost visible timeline item
|
||||||
/// and updates the floating date binding.
|
/// and updates the floating date binding.
|
||||||
func updateFloatingDate() {
|
func updateFloatingDate() {
|
||||||
guard let dateText = newestVisibleDateText() else {
|
guard let date = newestVisibleDate() else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,9 +537,9 @@ extension TimelineTableViewController {
|
|||||||
// to extend the display duration of the floating date.
|
// to extend the display duration of the floating date.
|
||||||
scheduleFloatingDateHide()
|
scheduleFloatingDateHide()
|
||||||
|
|
||||||
// Only update on changes to avoid needless SwiftUI recomputation.
|
// Only update when the calendar day changes to avoid needless SwiftUI recomputation.
|
||||||
if floatingDateText != dateText {
|
if floatingDate.map({ !Calendar.current.isDate($0, inSameDayAs: date) }) ?? true {
|
||||||
floatingDateText = dateText
|
floatingDate = date
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,18 +547,33 @@ extension TimelineTableViewController {
|
|||||||
func scheduleFloatingDateHide() {
|
func scheduleFloatingDateHide() {
|
||||||
floatingDateHideWorkItem?.cancel()
|
floatingDateHideWorkItem?.cancel()
|
||||||
let workItem = DispatchWorkItem { [weak self] in
|
let workItem = DispatchWorkItem { [weak self] in
|
||||||
self?.floatingDateText = nil
|
self?.floatingDate = nil
|
||||||
}
|
}
|
||||||
floatingDateHideWorkItem = workItem
|
floatingDateHideWorkItem = workItem
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: workItem)
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: workItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the formatted date text for the newest visible timeline item.
|
/// Scrolls to the first (oldest) item on the same calendar day as the current floating date.
|
||||||
|
private func scrollToFirstItemForCurrentDate() {
|
||||||
|
guard let floatingDate else { return }
|
||||||
|
// timelineItemsDictionary is ordered oldest-first; the first match is the earliest item for that day.
|
||||||
|
for uniqueID in timelineItemsDictionary.keys {
|
||||||
|
if let timestamp = timelineItemsDictionary[uniqueID]?.timestamp,
|
||||||
|
Calendar.current.isDate(timestamp, inSameDayAs: floatingDate),
|
||||||
|
let indexPath = dataSource?.indexPath(for: uniqueID) {
|
||||||
|
// The table view is flipped, so .bottom aligns the cell to the visual top.
|
||||||
|
tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the timestamp of the newest visible timeline item.
|
||||||
///
|
///
|
||||||
/// The table view is flipped (unless VoiceOver is running), so the "newest"
|
/// The table view is flipped (unless VoiceOver is running), so the "newest"
|
||||||
/// visible cell on screen is actually the *last* index path in `indexPathsForVisibleRows`
|
/// visible cell on screen is actually the *last* index path in `indexPathsForVisibleRows`
|
||||||
/// when the table is flipped, or the *first* when it is not.
|
/// when the table is flipped, or the *first* when it is not.
|
||||||
private func newestVisibleDateText() -> String? {
|
private func newestVisibleDate() -> Date? {
|
||||||
guard let visibleIndexPaths = tableView.indexPathsForVisibleRows,
|
guard let visibleIndexPaths = tableView.indexPathsForVisibleRows,
|
||||||
!visibleIndexPaths.isEmpty else {
|
!visibleIndexPaths.isEmpty else {
|
||||||
return nil
|
return nil
|
||||||
@@ -562,11 +584,11 @@ extension TimelineTableViewController {
|
|||||||
let isFlipped = scaleY == -1
|
let isFlipped = scaleY == -1
|
||||||
let orderedPaths = isFlipped ? visibleIndexPaths.reversed() : visibleIndexPaths
|
let orderedPaths = isFlipped ? visibleIndexPaths.reversed() : visibleIndexPaths
|
||||||
|
|
||||||
// Walk from topmost downward and return the date of the first item that has one.
|
// Walk from topmost downward and return the timestamp of the first item that has one.
|
||||||
for indexPath in orderedPaths {
|
for indexPath in orderedPaths {
|
||||||
if let uniqueID = dataSource?.itemIdentifier(for: indexPath),
|
if let uniqueID = dataSource?.itemIdentifier(for: indexPath),
|
||||||
let timestamp = timelineItemsDictionary[uniqueID]?.timestamp {
|
let timestamp = timelineItemsDictionary[uniqueID]?.timestamp {
|
||||||
return timestamp.formattedDateSeparator()
|
return timestamp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -178,6 +178,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
paginateForwards()
|
paginateForwards()
|
||||||
case .scrollToBottom:
|
case .scrollToBottom:
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
case .scrollToFirstItemForCurrentDate:
|
||||||
|
state.timelineState.scrollToFirstItemForDatePublisher.send()
|
||||||
case .displayTimelineItemMenu(let itemID):
|
case .displayTimelineItemMenu(let itemID):
|
||||||
timelineInteractionHandler.displayTimelineItemActionMenu(for: itemID)
|
timelineInteractionHandler.displayTimelineItemActionMenu(for: itemID)
|
||||||
case .handleTimelineItemMenuAction(let itemID, let action):
|
case .handleTimelineItemMenuAction(let itemID, let action):
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import SwiftUI
|
|||||||
/// fades out shortly after scrolling stops.
|
/// fades out shortly after scrolling stops.
|
||||||
struct FloatingDateBadge: View {
|
struct FloatingDateBadge: View {
|
||||||
let dateText: String?
|
let dateText: String?
|
||||||
|
var onTap: (() -> Void)?
|
||||||
|
|
||||||
@Environment(\.colorScheme) private var colorScheme: ColorScheme
|
@Environment(\.colorScheme) private var colorScheme: ColorScheme
|
||||||
|
|
||||||
@@ -28,14 +29,18 @@ struct FloatingDateBadge: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
if let dateText {
|
if let dateText {
|
||||||
Text(dateText)
|
Button { onTap?() } label: {
|
||||||
.font(.compound.bodySMSemibold)
|
Text(dateText)
|
||||||
.foregroundColor(.compound.textPrimary)
|
.font(.compound.bodySMSemibold)
|
||||||
.padding(.horizontal, 16)
|
.foregroundColor(.compound.textPrimary)
|
||||||
.padding(.vertical, 8)
|
.padding(.horizontal, 16)
|
||||||
.background(backgroundColor, in: .capsule)
|
.padding(.vertical, 8)
|
||||||
.shadow(color: Color(red: 0.11, green: 0.11, blue: 0.13).opacity(0.1), radius: 12, x: 0, y: 4)
|
.background(backgroundColor, in: .capsule)
|
||||||
.transition(.opacity)
|
.shadow(color: Color(red: 0.11, green: 0.11, blue: 0.13).opacity(0.1), radius: 12, x: 0, y: 4)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.disabled(onTap == nil)
|
||||||
|
.transition(.opacity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.animation(.easeInOut(duration: 0.15).disabledDuringTests(), value: dateText)
|
.animation(.easeInOut(duration: 0.15).disabledDuringTests(), value: dateText)
|
||||||
|
|||||||
@@ -83,8 +83,9 @@ struct TimelineViewRepresentable: UIViewControllerRepresentable {
|
|||||||
func makeUIViewController(context: Context) -> TimelineTableViewController {
|
func makeUIViewController(context: Context) -> TimelineTableViewController {
|
||||||
TimelineTableViewController(coordinator: context.coordinator,
|
TimelineTableViewController(coordinator: context.coordinator,
|
||||||
isScrolledToBottom: $viewModelContext.isScrolledToBottom,
|
isScrolledToBottom: $viewModelContext.isScrolledToBottom,
|
||||||
floatingDateText: $viewModelContext.floatingDateText,
|
floatingDate: $viewModelContext.floatingDate,
|
||||||
scrollToBottomPublisher: viewModelContext.viewState.timelineState.scrollToBottomPublisher)
|
scrollToBottomPublisher: viewModelContext.viewState.timelineState.scrollToBottomPublisher,
|
||||||
|
scrollToFirstItemForDatePublisher: viewModelContext.viewState.timelineState.scrollToFirstItemForDatePublisher)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIViewController(_ uiViewController: TimelineTableViewController, context: Context) {
|
func updateUIViewController(_ uiViewController: TimelineTableViewController, context: Context) {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f1fe8beb8c258e07ceff2aef6a7066c35aa2fbf23dc8de8c01166505554f1859
|
oid sha256:d508b5192fd7e8a860e4255a8587160d49e39c0eacb276409949f4d7d41e4b85
|
||||||
size 50048
|
size 42801
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f1fe8beb8c258e07ceff2aef6a7066c35aa2fbf23dc8de8c01166505554f1859
|
oid sha256:d508b5192fd7e8a860e4255a8587160d49e39c0eacb276409949f4d7d41e4b85
|
||||||
size 50048
|
size 42801
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:dde984085f2c4ebde7a482721ff1eff6f3c5d13c9e6a853756e150a2b3341384
|
oid sha256:8d3dfac7d6e711936a35fd179f0631aea2bf4e8d7587f467fea06893c272c7ec
|
||||||
size 38360
|
size 31011
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:dde984085f2c4ebde7a482721ff1eff6f3c5d13c9e6a853756e150a2b3341384
|
oid sha256:8d3dfac7d6e711936a35fd179f0631aea2bf4e8d7587f467fea06893c272c7ec
|
||||||
size 38360
|
size 31011
|
||||||
|
|||||||
Reference in New Issue
Block a user