Fix a bug where the timeline disappeared when VoiceOver was enabled. (#4701)

This commit is contained in:
Doug
2025-11-05 10:09:09 +00:00
committed by GitHub
parent ee5d57606c
commit 1fc41aba75
3 changed files with 47 additions and 43 deletions
@@ -0,0 +1,27 @@
//
// Copyright 2025 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import SwiftUI
extension View {
/// Overlays the given banner view at the top edge of this view, using a
/// slide from the top edge when `isVisible` is toggled.
func topBanner(_ banner: some View, isVisible: Bool) -> some View {
overlay(alignment: .top) {
ZStack {
if isVisible {
banner.transition(.move(edge: .top))
} else {
// An equal amount of space needs to be reserved in order for the transition to work.
banner.hidden()
}
}
.animation(.elementDefault, value: isVisible)
.clipped()
}
}
}
@@ -33,20 +33,15 @@ struct RoomScreen: View {
.accessibilityIdentifier(A11yIdentifiers.roomScreen.scrollToBottom)
}
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.overlay(alignment: .top) {
if !isVoiceOverEnabled {
pinnedItemsBanner
}
}
.topBanner(pinnedItemsBanner, isVisible: context.viewState.shouldShowPinnedEventsBanner && !isVoiceOverEnabled)
// This can overlay on top of the pinnedItemsBanner
.overlay(alignment: .top) {
knockRequestsBanner
}
.topBanner(knockRequestsBanner, isVisible: context.viewState.shouldSeeKnockRequests)
.safeAreaInset(edge: .top) {
// When voice over is on the table view is not reversed
// and the scroll gestures are not intercepted
// so we render the pinned banner on top.
if isVoiceOverEnabled {
// 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.
if context.viewState.shouldShowPinnedEventsBanner, isVoiceOverEnabled {
pinnedItemsBanner
}
}
@@ -79,41 +74,19 @@ struct RoomScreen: View {
@ViewBuilder
private var pinnedItemsBanner: some View {
// Color.clear and clipped() are required for iOS 26 transparent nav bar
VStack(spacing: 0) {
if context.viewState.shouldShowPinnedEventsBanner {
PinnedItemsBannerView(state: context.viewState.pinnedEventsBannerState,
onMainButtonTap: { context.send(viewAction: .tappedPinnedEventsBanner) },
onViewAllButtonTap: { context.send(viewAction: .viewAllPins) })
.transition(.move(edge: .top))
} else {
Color.clear
.allowsHitTesting(false)
}
}
.animation(.elementDefault, value: context.viewState.shouldShowPinnedEventsBanner)
.clipped()
PinnedItemsBannerView(state: context.viewState.pinnedEventsBannerState,
onMainButtonTap: { context.send(viewAction: .tappedPinnedEventsBanner) },
onViewAllButtonTap: { context.send(viewAction: .viewAllPins) })
}
@ViewBuilder
private var knockRequestsBanner: some View {
// Color.clear and clipped() are required for iOS 26 transparent nav bar
VStack(spacing: 0) {
if context.viewState.shouldSeeKnockRequests {
KnockRequestsBannerView(requests: context.viewState.displayedKnockRequests,
onDismiss: dismissKnockRequestsBanner,
onAccept: context.viewState.canAcceptKnocks ? acceptKnockRequest : nil,
onViewAll: onViewAllKnockRequests,
mediaProvider: context.mediaProvider)
.padding(.top, 16)
.transition(.move(edge: .top))
} else {
Color.clear
.allowsHitTesting(false)
}
}
.animation(.elementDefault, value: context.viewState.shouldSeeKnockRequests)
.clipped()
KnockRequestsBannerView(requests: context.viewState.displayedKnockRequests,
onDismiss: dismissKnockRequestsBanner,
onAccept: context.viewState.canAcceptKnocks ? acceptKnockRequest : nil,
onViewAll: onViewAllKnockRequests,
mediaProvider: context.mediaProvider)
.padding(.top, 16)
}
private func dismissKnockRequestsBanner() {