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 62a64f4882
commit 7a96a42c6b
3 changed files with 47 additions and 43 deletions

View File

@@ -164,6 +164,7 @@
1A83DD22F3E6F76B13B6E2F9 /* VideoRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C8616254EE40CA8BA5E9BC2 /* VideoRoomTimelineItemContent.swift */; };
1AB3D8563AB12635250A6A6E /* StaticLocationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C15E0017717EAE3A1D02D005 /* StaticLocationScreenCoordinator.swift */; };
1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */; };
1AF4A82B4332CAD2DB3EB5DA /* TopBannerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78483F0143E185EDC6ECD741 /* TopBannerModifier.swift */; };
1B2DADC008EE211AF1DA5292 /* NotificationManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ED584467DB380E3CEFB1DB /* NotificationManagerTests.swift */; };
1B2F9F368619FFF8C63C87CC /* BugReportScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EECE8B331CD169790EF284F /* BugReportScreenViewModelTests.swift */; };
1B5B30839656AE2F957C6B1E /* test_pdf.pdf in Resources */ = {isa = PBXBuildFile; fileRef = BE98688578F8B0541D853695 /* test_pdf.pdf */; };
@@ -2115,6 +2116,7 @@
7720ACAC6155AB7F9C70B546 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nb; path = nb.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
7773CBFDBD458E0B7E270507 /* PillView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillView.swift; sourceTree = "<group>"; };
780258F1B9D15E30549FF4BE /* NotificationSettingsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModel.swift; sourceTree = "<group>"; };
78483F0143E185EDC6ECD741 /* TopBannerModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopBannerModifier.swift; sourceTree = "<group>"; };
787E84119E626E2F0E0BFBE8 /* ManageRoomMemberSheetModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetModels.swift; sourceTree = "<group>"; };
78910787F967CBC6042A101E /* StartChatScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModelProtocol.swift; sourceTree = "<group>"; };
78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitCoordinatorTests.swift; sourceTree = "<group>"; };
@@ -3599,6 +3601,7 @@
A8558D41DD4B553A752C868A /* StackedAvatarsView.swift */,
E10765FBC83B34A3BC4ADB23 /* TimelineScrollToBottomButton.swift */,
16D353E10A64172D863769BF /* TombstonedAvatarImage.swift */,
78483F0143E185EDC6ECD741 /* TopBannerModifier.swift */,
E10DA51DBC8C7E1460DBCCBD /* UserProfileListRow.swift */,
AD529C89924EE32CE307F36F /* VisualListItem.swift */,
);
@@ -8424,6 +8427,7 @@
98EE4259A4A49BC757BA442C /* TimelineViewModel.swift in Sources */,
F8B2F5CBCF2A0E0798E8D646 /* TimelineViewModelProtocol.swift in Sources */,
B3D8AA9988F8A000B162DCB5 /* TombstonedAvatarImage.swift in Sources */,
1AF4A82B4332CAD2DB3EB5DA /* TopBannerModifier.swift in Sources */,
6E44638FDF7D4B0F80EFA7EA /* TraceLogPack.swift in Sources */,
126CBCF5B0145FA1377C1316 /* Tracing.swift in Sources */,
E010DDE938032D3B8E84CC35 /* TracingHook.swift in Sources */,

View File

@@ -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()
}
}
}

View File

@@ -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() {