diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 7f9672a02..4538badb2 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -1286,6 +1286,7 @@ FCD3F2B82CAB29A07887A127 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 2B43F2AF7456567FE37270A7 /* KeychainAccess */; }; FCF95603F1D056B1B106A415 /* AdvancedSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83E2B20431F890ED64255CA1 /* AdvancedSettingsScreenViewModelProtocol.swift */; }; FD29471C72872F8B7580E3E1 /* KeychainControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C0D861FC397AC34BCF089E /* KeychainControllerMock.swift */; }; + FD439E183A48BE871AEEFAEA /* TimelineScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10765FBC83B34A3BC4ADB23 /* TimelineScrollToBottomButton.swift */; }; FD4C21F8DA1E273DE94FCD1A /* NotificationItemProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B927CF5EF7FCCDA5EDC474B /* NotificationItemProxyProtocol.swift */; }; FD573B5D665824EB79EABF06 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5327E3B3C58BEB0E65F4CF98 /* Observable.swift */; }; FD762761C5D0C30E6255C3D8 /* ServerConfirmationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */; }; @@ -2480,6 +2481,7 @@ E06AAD6D9D3F5833E7A5A2F9 /* RoomListFilterModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListFilterModels.swift; sourceTree = ""; }; E0FCA0957FAA0E15A9F5579D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Untranslated.stringsdict; sourceTree = ""; }; E0FF9CB3EFA753277291F609 /* EncryptionResetScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetScreenCoordinator.swift; sourceTree = ""; }; + E10765FBC83B34A3BC4ADB23 /* TimelineScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineScrollToBottomButton.swift; sourceTree = ""; }; E10DA51DBC8C7E1460DBCCBD /* UserProfileListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileListRow.swift; sourceTree = ""; }; E157152B11E347F735C3FD6E /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = tr; path = tr.lproj/Localizable.stringsdict; sourceTree = ""; }; E1573D28C8A9FB6399D0EEFB /* SecureBackupLogoutConfirmationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenCoordinator.swift; sourceTree = ""; }; @@ -3354,6 +3356,7 @@ 839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */, AEB5FF7A09B79B0C6B528F7C /* SFNumberedListView.swift */, A8558D41DD4B553A752C868A /* StackedAvatarsView.swift */, + E10765FBC83B34A3BC4ADB23 /* TimelineScrollToBottomButton.swift */, 16D353E10A64172D863769BF /* TombstonedAvatarImage.swift */, E10DA51DBC8C7E1460DBCCBD /* UserProfileListRow.swift */, AD529C89924EE32CE307F36F /* VisualListItem.swift */, @@ -7786,6 +7789,7 @@ 8446C2A7ECEFDA79F622725F /* TimelineReactionsView.swift in Sources */, 4DAEE2468669848B6C9F55B4 /* TimelineReadReceiptsView.swift in Sources */, 6EB46C92ECFEAE71959D91D2 /* TimelineReplyView.swift in Sources */, + FD439E183A48BE871AEEFAEA /* TimelineScrollToBottomButton.swift in Sources */, 9FBE1FB20171012260A32492 /* TimelineSenderAvatarView.swift in Sources */, C4FE0E11A907C8999F92D5A8 /* TimelineStartRoomTimelineItem.swift in Sources */, 785613C0C092B532198EB3BB /* TimelineStartRoomTimelineView.swift in Sources */, diff --git a/ElementX/Sources/Other/SwiftUI/Views/TimelineScrollToBottomButton.swift b/ElementX/Sources/Other/SwiftUI/Views/TimelineScrollToBottomButton.swift new file mode 100644 index 000000000..078a433ac --- /dev/null +++ b/ElementX/Sources/Other/SwiftUI/Views/TimelineScrollToBottomButton.swift @@ -0,0 +1,34 @@ +// +// Copyright 2025 New Vector 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 + +struct TimelineScrollToBottomButton: View { + let isVisible: Bool + let callback: () -> Void + + var body: some View { + Button { callback() } label: { + Image(systemName: "chevron.down") + .font(.compound.bodyLG) + .fontWeight(.semibold) + .foregroundColor(.compound.iconSecondary) + .padding(13) + .offset(y: 1) + .background { + Circle() + .fill(Color.compound.iconOnSolidPrimary) + // Intentionally using system primary colour to get white/black. + .shadow(color: .primary.opacity(0.33), radius: 2.0) + } + .padding() + } + .opacity(isVisible ? 0.0 : 1.0) + .accessibilityHidden(isVisible) + .animation(.elementDefault, value: isVisible) + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 40444d9d0..0da6a6a11 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -28,7 +28,10 @@ struct RoomScreen: View { var body: some View { TimelineView(timelineContext: timelineContext) .overlay(alignment: .bottomTrailing) { - scrollToBottomButton + TimelineScrollToBottomButton(isVisible: isAtBottomAndLive) { + timelineContext.send(viewAction: .scrollToBottom) + } + .accessibilityIdentifier(A11yIdentifiers.roomScreen.scrollToBottom) } .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) .overlay(alignment: .top) { @@ -123,28 +126,6 @@ struct RoomScreen: View { context.send(viewAction: .viewKnockRequests) } - private var scrollToBottomButton: some View { - Button { timelineContext.send(viewAction: .scrollToBottom) } label: { - Image(systemName: "chevron.down") - .font(.compound.bodyLG) - .fontWeight(.semibold) - .foregroundColor(.compound.iconSecondary) - .padding(13) - .offset(y: 1) - .background { - Circle() - .fill(Color.compound.iconOnSolidPrimary) - // Intentionally using system primary colour to get white/black. - .shadow(color: .primary.opacity(0.33), radius: 2.0) - } - .padding() - } - .opacity(isAtBottomAndLive ? 0.0 : 1.0) - .accessibilityHidden(isAtBottomAndLive) - .animation(.elementDefault, value: isAtBottomAndLive) - .accessibilityIdentifier(A11yIdentifiers.roomScreen.scrollToBottom) - } - private var isAtBottomAndLive: Bool { timelineContext.isScrolledToBottom && timelineContext.viewState.timelineState.isLive } diff --git a/ElementX/Sources/Screens/ThreadTimelineScreen/View/ThreadTimelineScreen.swift b/ElementX/Sources/Screens/ThreadTimelineScreen/View/ThreadTimelineScreen.swift index d9d8a8e22..d7f5e25bc 100644 --- a/ElementX/Sources/Screens/ThreadTimelineScreen/View/ThreadTimelineScreen.swift +++ b/ElementX/Sources/Screens/ThreadTimelineScreen/View/ThreadTimelineScreen.swift @@ -32,7 +32,9 @@ struct ThreadTimelineScreen: View { .toolbarBackground(.visible, for: .navigationBar) // Fix the toolbar's background. .timelineMediaPreview(viewModel: $context.mediaPreviewViewModel) .overlay(alignment: .bottomTrailing) { - scrollToBottomButton + TimelineScrollToBottomButton(isVisible: isAtBottomAndLive) { + timelineContext.send(viewAction: .scrollToBottom) + } } .safeAreaInset(edge: .bottom, spacing: 0) { composer @@ -75,28 +77,6 @@ struct ThreadTimelineScreen: View { } } - private var scrollToBottomButton: some View { - Button { timelineContext.send(viewAction: .scrollToBottom) } label: { - Image(systemName: "chevron.down") - .font(.compound.bodyLG) - .fontWeight(.semibold) - .foregroundColor(.compound.iconSecondary) - .padding(13) - .offset(y: 1) - .background { - Circle() - .fill(Color.compound.iconOnSolidPrimary) - // Intentionally using system primary colour to get white/black. - .shadow(color: .primary.opacity(0.33), radius: 2.0) - } - .padding() - } - .opacity(isAtBottomAndLive ? 0.0 : 1.0) - .accessibilityHidden(isAtBottomAndLive) - .animation(.elementDefault, value: isAtBottomAndLive) - .accessibilityIdentifier(A11yIdentifiers.roomScreen.scrollToBottom) - } - private var isAtBottomAndLive: Bool { timelineContext.isScrolledToBottom && timelineContext.viewState.timelineState.isLive }