Remove the "unflip" of the timeline when in voice over mode (#5835)

Keep the timeline as it is in voice over mode, and don't "unflip" it
This commit is contained in:
Mauro
2026-07-06 14:00:53 +02:00
committed by GitHub
parent b8e6421551
commit d2ef1e71f9
2 changed files with 12 additions and 30 deletions
@@ -81,10 +81,10 @@ struct RoomScreen: View {
TopBannerLayer(knockRequestsBanner, isVisible: context.viewState.shouldSeeKnockRequests)
], footer: dateBadge)
.safeAreaInset(edge: .top) {
// When VoiceOver is enabled, the table view isn't reversed and the scroll gestures
// don't trigger meaning the banner never hides itself and so the .overlay layout
// above permanently obscures the top of the timeline. So whenever VoiceOver is
// enabled we use a safe area inset to vertically stack it above the timeline.
// When VoiceOver is enabled the scroll gestures don't trigger, so the banner never
// hides itself and the .overlay layout above would permanently obscure the top of
// the timeline. So whenever VoiceOver is enabled we use a safe area inset to
// vertically stack it above the timeline instead.
if context.viewState.shouldShowPinnedEventsBanner || context.viewState.isSharingLiveLocation, isVoiceOverEnabled {
VStack(spacing: 0) {
if context.viewState.shouldShowPinnedEventsBanner {
@@ -180,11 +180,6 @@ class TimelineTableViewController: UIViewController {
/// Whether or not the view has been shown on screen yet.
private var hasAppearedOnce = false
/// Value that determines if the table view is flipped or not according to the VoiceOver status.
private var scaleY: CGFloat {
UIAccessibility.isVoiceOverRunning ? 1 : -1
}
init(coordinator: TimelineViewRepresentable.Coordinator,
isScrolledToBottom: Binding<Bool>,
isReadMarkerVisible: Binding<Bool>,
@@ -208,9 +203,8 @@ class TimelineTableViewController: UIViewController {
tableView.keyboardDismissMode = .onDrag
tableView.backgroundColor = .compound.bgCanvasDefault
// The tableview should be flipped to display the newest items at the top
// the only exception is VoiceOver, where we want to keep the latest item at the top as Android.
tableView.transform = CGAffineTransform(scaleX: 1, y: scaleY)
// The tableview is flipped to display the newest items at the bottom.
tableView.transform = CGAffineTransform(scaleX: 1, y: -1)
view.addSubview(tableView)
// Prevents XCUITest from invoking the diffable dataSource's cellProvider
@@ -256,15 +250,6 @@ class TimelineTableViewController: UIViewController {
}
.store(in: &cancellables)
// Observe voice over status changes to flip the table view accordingly
NotificationCenter.default.publisher(for: UIAccessibility.voiceOverStatusDidChangeNotification)
.sink { [weak self] _ in
guard let self else { return }
tableView.transform = CGAffineTransform(scaleX: 1, y: scaleY)
tableView.reloadData()
}
.store(in: &cancellables)
configureDataSource()
}
@@ -312,7 +297,7 @@ class TimelineTableViewController: UIViewController {
.background(Color.clear)
// Flipping the cell can create some issues with cell resizing, so flip the content View
cell.contentView.transform = CGAffineTransform(scaleX: 1, y: scaleY)
cell.contentView.transform = CGAffineTransform(scaleX: 1, y: -1)
cell.accessibilityElements = [cell.contentView] // Ensure VoiceOver reads the content view only
return cell
@@ -339,7 +324,7 @@ class TimelineTableViewController: UIViewController {
.background(Color.clear)
// Flipping the cell can create some issues with cell resizing, so flip the content View
cell.contentView.transform = CGAffineTransform(scaleX: 1, y: scaleY)
cell.contentView.transform = CGAffineTransform(scaleX: 1, y: -1)
return cell
}
}
@@ -633,19 +618,16 @@ extension TimelineTableViewController {
/// Returns the timestamp of the newest visible timeline item.
///
/// The table view is flipped (unless VoiceOver is running), so the "newest"
/// visible cell on screen is actually the *last* index path in `indexPathsForVisibleRows`
/// when the table is flipped, or the *first* when it is not.
/// The table view is flipped, so the "newest" visible cell on screen is
/// actually the *last* index path in `indexPathsForVisibleRows`.
private func newestVisibleDate() -> Date? {
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
// In a flipped table view the last index path is the topmost item on screen.
let orderedPaths = visibleIndexPaths.reversed()
// Walk from topmost downward and return the timestamp of the first item that has one.
for indexPath in orderedPaths {