165 lines
6.5 KiB
Swift
165 lines
6.5 KiB
Swift
//
|
|
// 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 Combine
|
|
import Compound
|
|
import SwiftUI
|
|
|
|
/// The view shown when the user isn't part of any rooms.
|
|
struct HomeScreenEmptyStateView: View {
|
|
let context: HomeScreenViewModel.Context
|
|
|
|
var body: some View {
|
|
VStack(spacing: 6) {
|
|
Text(L10n.screenRoomlistEmptyTitle)
|
|
.font(.compound.bodyLG)
|
|
.foregroundColor(.compound.textSecondary)
|
|
.multilineTextAlignment(.center)
|
|
|
|
Text(L10n.screenRoomlistEmptyMessage)
|
|
.font(.compound.bodyLG)
|
|
.foregroundColor(.compound.textSecondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.bottom, 12)
|
|
|
|
Button { context.send(viewAction: .startChat) } label: {
|
|
Label(L10n.actionStartChat, icon: \.edit)
|
|
.font(.compound.bodyLGSemibold)
|
|
.foregroundColor(.compound.textOnSolidPrimary)
|
|
.padding(.vertical, 6)
|
|
.padding(.horizontal, 22)
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.buttonBorderShape(.capsule)
|
|
}
|
|
.padding(16)
|
|
}
|
|
}
|
|
|
|
/// A custom layout for the empty state which will show it centrally with the
|
|
/// session verification banner and invites button stacked at the top.
|
|
struct HomeScreenEmptyStateLayout: Layout {
|
|
/// The vertical spacing between views in the layout.
|
|
var spacing: CGFloat = 8
|
|
/// The minimum height of the layout. This should be the height of the scroll view.
|
|
var minHeight: CGFloat = 0
|
|
|
|
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
|
// We keep the proposed width and replace the height with the minimum specified,
|
|
// or the total height of the subviews if it exceeds the minimum height.
|
|
let width = proposal.width ?? .greatestFiniteMagnitude
|
|
var height: CGFloat = spacing * CGFloat(max(0, subviews.count - 1))
|
|
|
|
for subview in subviews {
|
|
let size = subview.sizeThatFits(proposal)
|
|
height += size.height
|
|
}
|
|
|
|
return CGSize(width: width, height: max(minHeight, height))
|
|
}
|
|
|
|
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
|
|
let mainView = subviews.first(where: { $0.priority > 0 })
|
|
let topViews = subviews.filter { $0 != mainView }
|
|
|
|
var y: CGFloat = bounds.minY
|
|
|
|
// Place all the top views in a vertical stack, centering horizontally.
|
|
for view in topViews {
|
|
let size = view.sizeThatFits(proposal)
|
|
let x = (bounds.width - size.width) / 2
|
|
view.place(at: CGPoint(x: x, y: y), proposal: proposal)
|
|
y += size.height + spacing
|
|
}
|
|
|
|
// Place the main view in the center if there is space, otherwise add it to the stack.
|
|
guard let mainView else { return }
|
|
|
|
let mainViewSize = mainView.sizeThatFits(proposal)
|
|
if (y + mainViewSize.height / 2) < bounds.height / 2 {
|
|
let center = CGPoint(x: bounds.midX, y: bounds.midY)
|
|
mainView.place(at: center, anchor: .center, proposal: proposal)
|
|
} else {
|
|
let x = (bounds.width - mainViewSize.width) / 2
|
|
mainView.place(at: CGPoint(x: x, y: y), proposal: proposal)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Previews
|
|
|
|
struct HomeScreenEmptyStateView_Previews: PreviewProvider, TestablePreview {
|
|
static var previews: some View {
|
|
HomeScreenEmptyStateView(context: viewModel.context)
|
|
.previewDisplayName("View")
|
|
|
|
GeometryReader { geometry in
|
|
ScrollView {
|
|
HomeScreenEmptyStateLayout(minHeight: geometry.size.height) {
|
|
banner
|
|
|
|
HomeScreenEmptyStateView(context: viewModel.context)
|
|
.layoutPriority(1)
|
|
}
|
|
}
|
|
}
|
|
.previewDisplayName("Normal Layout")
|
|
|
|
GeometryReader { geometry in
|
|
ScrollView {
|
|
HomeScreenEmptyStateLayout(minHeight: geometry.size.height) {
|
|
banner
|
|
banner
|
|
banner
|
|
|
|
HomeScreenEmptyStateView(context: viewModel.context)
|
|
.layoutPriority(1)
|
|
}
|
|
}
|
|
}
|
|
.previewDisplayName("Constrained layout")
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
static var banner: some View {
|
|
Text("This is a title that is very long")
|
|
.font(.compound.headingXLBold)
|
|
.multilineTextAlignment(.center)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
.background {
|
|
RoundedRectangle(cornerRadius: 20)
|
|
.fill(Color.compound.bgSubtleSecondary)
|
|
}
|
|
.padding()
|
|
}
|
|
|
|
static let viewModel = {
|
|
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@user:example.com",
|
|
roomSummaryProvider: MockRoomSummaryProvider(state: .loaded([]))),
|
|
mediaProvider: MockMediaProvider(),
|
|
voiceMessageMediaManager: VoiceMessageMediaManagerMock())
|
|
|
|
return HomeScreenViewModel(userSession: userSession,
|
|
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder()),
|
|
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
|
|
appSettings: ServiceLocator.shared.settings,
|
|
analytics: ServiceLocator.shared.analytics,
|
|
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
|
}()
|
|
}
|