Migration Screen (#1224)

- Hide bottom bar when in skeletons.
- Use the new design for the Waitlist screen too.
This commit is contained in:
Doug
2023-06-30 13:27:49 +01:00
committed by GitHub
parent c5d47378a4
commit 2025dd9517
38 changed files with 528 additions and 52 deletions
@@ -0,0 +1,93 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
/// A standardised layout for waiting dialogs shown during the onboarding flow.
struct WaitingDialog<Content: View, BottomContent: View>: View {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(\.verticalSizeClass) private var verticalSizeClass
/// Not using the default to allow the gradient to fill edge to edge.
private let horizontalPadding = 16.0
/// The top padding of the content, used to keep the text below the image.
private var contentTopPadding: CGFloat {
if verticalSizeClass == .compact {
// Reduced value for iPhones in landscape.
return UIConstants.onboardingBreakerScreenTopPadding
} else if horizontalSizeClass == .compact {
// The default value for portrait iPhones.
return 2 * UIConstants.onboardingBreakerScreenTopPadding
} else {
// Larger on iPad specifically for 11" in Landscape.
return 2.7 * UIConstants.onboardingBreakerScreenTopPadding
}
}
/// The main content shown at the top of the layout.
@ViewBuilder var content: () -> Content
/// The content shown at the bottom of the layout.
@ViewBuilder var bottomContent: () -> BottomContent
var body: some View {
FullscreenDialog(topPadding: UIConstants.onboardingBreakerScreenTopPadding, horizontalPadding: 0) {
content()
.padding(.top, contentTopPadding)
.padding(.horizontal, horizontalPadding)
} bottomContent: {
bottomContent()
.padding(.horizontal, horizontalPadding)
}
.background {
background
.ignoresSafeArea(edges: [.horizontal, .bottom])
}
.ignoresSafeArea(edges: [.horizontal, .bottom])
.environment(\.backgroundStyle, AnyShapeStyle(Color.compound.bgCanvasDefault))
.environment(\.colorScheme, .dark)
.toolbar(.visible, for: .navigationBar) // Layout consistency in all states.
.toolbarBackground(.hidden, for: .navigationBar)
.preferredColorScheme(.light) // Has no effect on Previews, works fine in-app.
}
var background: some View {
// Negative spacing to hide a rendering bug that shows the white background.
GeometryReader { geometry in
VStack(spacing: -5) {
Color.white
.frame(maxHeight: UIConstants.onboardingBreakerScreenTopPadding)
Image(asset: Asset.Images.waitingGradient)
.resizable()
.frame(width: horizontalSizeClass == .compact ? nil : geometry.size.width) // Forced for landscape iPhone and nicer iPad.
.scaledToFit()
.layoutPriority(1) // Force for all landscape layouts.
Color.compound.bgCanvasDefault
}
}
}
}
struct WaitingDialog_Previews: PreviewProvider {
static let viewModel = WaitlistScreenViewModel(homeserver: .mockMatrixDotOrg)
static var previews: some View {
NavigationStack {
WaitlistScreen(context: viewModel.context)
}
}
}
@@ -20,42 +20,29 @@ struct WaitlistScreen: View {
@ObservedObject var context: WaitlistScreenViewModel.Context
var body: some View {
FullscreenDialog(topPadding: UIConstants.iconTopPaddingToNavigationBar) {
header
WaitingDialog {
content
} bottomContent: {
buttons
}
.background()
.environment(\.backgroundStyle, AnyShapeStyle(Color.compound.bgCanvasDefault))
.navigationBarBackButtonHidden()
.toolbar { toolbar }
.toolbar(.visible, for: .navigationBar) // Layout consistency in all states.
.overlay {
EffectsView(effect: context.viewState.isWaiting ? .none : .confetti)
.ignoresSafeArea()
.allowsHitTesting(false)
}
}
/// The main content of the view to be shown in a scroll view.
var header: some View {
VStack(spacing: 8) {
AuthenticationIconImage(image: Image(systemName: context.viewState.iconSymbolName))
.fontWeight(.semibold)
.padding(.bottom, 8)
Text(context.viewState.title)
.font(.compound.headingMDBold)
var content: some View {
VStack(spacing: 16) {
Text(context.viewState.title.tinting(".", color: .element.brand))
.font(.compound.headingXLBold)
.multilineTextAlignment(.center)
.foregroundColor(.compound.textPrimary)
.fixedSize(horizontal: false, vertical: true)
Text(context.viewState.message)
.font(.compound.bodyMD)
.font(.compound.bodyLG)
.multilineTextAlignment(.center)
.foregroundColor(.compound.textSecondary)
.foregroundColor(.compound.textPrimary)
}
.padding(.horizontal, 16)
}
/// The action buttons shown at the bottom of the view.
@@ -26,9 +26,17 @@ struct HomeScreen: View {
@ObservedObject var context: HomeScreenViewModel.Context
@State private var scrollViewAdapter = ScrollViewAdapter()
@State private var showingBottomToolbar = true
@State private var lastScrollDirection: ScrollViewAdapter.ScrollDirection = .up
@State private var isSearching = false
var bottomBarVisibility: Visibility {
if lastScrollDirection == .up, context.viewState.roomListMode == .rooms {
return .automatic
} else {
return .hidden
}
}
var body: some View {
ScrollView {
if context.viewState.showSessionVerificationBanner {
@@ -81,7 +89,7 @@ struct HomeScreen: View {
}
.onReceive(scrollViewAdapter.scrollDirection) { direction in
withAnimation(.elementDefault) {
showingBottomToolbar = (direction == .down)
lastScrollDirection = direction
}
}
.onChange(of: context.viewState.visibleRooms) { _ in
@@ -100,7 +108,7 @@ struct HomeScreen: View {
actions: leaveRoomAlertActions,
message: leaveRoomAlertMessage)
.navigationTitle(L10n.screenRoomlistMainSpaceTitle)
.toolbar(showingBottomToolbar ? .automatic : .hidden, for: .bottomBar)
.toolbar(bottomBarVisibility, for: .bottomBar)
.toolbar { toolbar }
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.track(screen: .home)
@@ -117,6 +125,8 @@ struct HomeScreen: View {
ToolbarItemGroup(placement: .bottomBar) {
Spacer()
newRoomButton
// Fix position animating on loop by getting caught up in the shimmer effect somehow.
.animation(.noAnimation, value: context.viewState.roomListMode)
}
}
@@ -0,0 +1,33 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import SwiftUI
final class MigrationScreenCoordinator: CoordinatorProtocol {
private var viewModel: MigrationScreenViewModelProtocol
private var cancellables: Set<AnyCancellable> = .init()
init() {
viewModel = MigrationScreenViewModel()
}
func start() { }
func toPresentable() -> AnyView {
AnyView(MigrationScreen(context: viewModel.context))
}
}
@@ -0,0 +1,23 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
enum MigrationScreenViewModelAction { }
struct MigrationScreenViewState: BindableState { }
enum MigrationScreenViewAction { }
@@ -0,0 +1,32 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import SwiftUI
typealias MigrationScreenViewModelType = StateStoreViewModel<MigrationScreenViewState, MigrationScreenViewAction>
class MigrationScreenViewModel: MigrationScreenViewModelType, MigrationScreenViewModelProtocol {
private var actionsSubject: PassthroughSubject<MigrationScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<MigrationScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init() {
super.init(initialViewState: MigrationScreenViewState())
}
}
@@ -0,0 +1,23 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
@MainActor
protocol MigrationScreenViewModelProtocol {
var actions: AnyPublisher<MigrationScreenViewModelAction, Never> { get }
var context: MigrationScreenViewModelType.Context { get }
}
@@ -0,0 +1,57 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
struct MigrationScreen: View {
@ObservedObject var context: MigrationScreenViewModel.Context
var body: some View {
WaitingDialog {
content
} bottomContent: {
EmptyView()
}
.navigationBarBackButtonHidden()
}
var content: some View {
VStack(spacing: 16) {
ProgressView()
.tint(.compound.iconPrimary)
.padding(.bottom, 4)
Text(L10n.screenMigrationTitle.tinting(".", color: .element.brand))
.font(.compound.headingXLBold)
.multilineTextAlignment(.center)
.foregroundColor(.compound.textPrimary)
Text(L10n.screenMigrationMessage)
.font(.compound.bodyLG)
.multilineTextAlignment(.center)
.foregroundColor(.compound.textPrimary)
}
}
}
// MARK: - Previews
struct MigrationScreen_Previews: PreviewProvider {
static let viewModel = MigrationScreenViewModel()
static var previews: some View {
MigrationScreen(context: viewModel.context)
}
}