mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Add an empty state on the home screen. (#1450)
This commit is contained in:
parent
46d5c673d4
commit
4eed77942f
@ -324,6 +324,7 @@
|
|||||||
76BA28216FBAF83B2D86A027 /* InvitesScreenCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2A71915C1F075E403F559C /* InvitesScreenCell.swift */; };
|
76BA28216FBAF83B2D86A027 /* InvitesScreenCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2A71915C1F075E403F559C /* InvitesScreenCell.swift */; };
|
||||||
7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */; };
|
7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */; };
|
||||||
77920AFA8091AC6B9F190C90 /* Signposter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752A0EB49BF5BCEA37EDF7A3 /* Signposter.swift */; };
|
77920AFA8091AC6B9F190C90 /* Signposter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752A0EB49BF5BCEA37EDF7A3 /* Signposter.swift */; };
|
||||||
|
77BB228AEA861E50FFD6A228 /* HomeScreenEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */; };
|
||||||
77C1A2F49CD90D3EFDF376E5 /* MapTilerURLBuildersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376D941BF8BB294389C0DE24 /* MapTilerURLBuildersTests.swift */; };
|
77C1A2F49CD90D3EFDF376E5 /* MapTilerURLBuildersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376D941BF8BB294389C0DE24 /* MapTilerURLBuildersTests.swift */; };
|
||||||
77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */; };
|
77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */; };
|
||||||
77FACC29F98FE2E65BBB6A5F /* ServerSelectionUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */; };
|
77FACC29F98FE2E65BBB6A5F /* ServerSelectionUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */; };
|
||||||
@ -1335,6 +1336,7 @@
|
|||||||
C070FD43DC6BF4E50217965A /* LocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationTests.swift; sourceTree = "<group>"; };
|
C070FD43DC6BF4E50217965A /* LocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationTests.swift; sourceTree = "<group>"; };
|
||||||
C08E9043618AE5B0BF7B07E1 /* TemplateScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModelTests.swift; sourceTree = "<group>"; };
|
C08E9043618AE5B0BF7B07E1 /* TemplateScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedMessageTimelineItemProtocol.swift; sourceTree = "<group>"; };
|
C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedMessageTimelineItemProtocol.swift; sourceTree = "<group>"; };
|
||||||
|
C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenEmptyStateView.swift; sourceTree = "<group>"; };
|
||||||
C1198B925F4A88DA74083662 /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = "<group>"; };
|
C1198B925F4A88DA74083662 /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = "<group>"; };
|
||||||
C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDirection.swift; sourceTree = "<group>"; };
|
C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDirection.swift; sourceTree = "<group>"; };
|
||||||
C1511766C534367700C8DD75 /* RoomNotificationModeProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationModeProxy.swift; sourceTree = "<group>"; };
|
C1511766C534367700C8DD75 /* RoomNotificationModeProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationModeProxy.swift; sourceTree = "<group>"; };
|
||||||
@ -2209,6 +2211,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
B902EA6CD3296B0E10EE432B /* HomeScreen.swift */,
|
B902EA6CD3296B0E10EE432B /* HomeScreen.swift */,
|
||||||
|
C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */,
|
||||||
24227FF9A2797F6EA7F69CDD /* HomeScreenInvitesButton.swift */,
|
24227FF9A2797F6EA7F69CDD /* HomeScreenInvitesButton.swift */,
|
||||||
ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */,
|
ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */,
|
||||||
C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */,
|
C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */,
|
||||||
@ -3670,7 +3673,7 @@
|
|||||||
path = Timeline;
|
path = Timeline;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
"TEMP_F85F8A84-6E1F-4213-A180-41C5B49354D2" /* element-x-ios */ = {
|
"TEMP_9E86D325-D1C7-441E-B1F2-A3D93615813E" /* element-x-ios */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
41553551C55AD59885840F0E /* secrets.xcconfig */,
|
41553551C55AD59885840F0E /* secrets.xcconfig */,
|
||||||
@ -4364,6 +4367,7 @@
|
|||||||
4295E5F850897710A51AE114 /* GeoURI.swift in Sources */,
|
4295E5F850897710A51AE114 /* GeoURI.swift in Sources */,
|
||||||
964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */,
|
964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */,
|
||||||
8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */,
|
8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */,
|
||||||
|
77BB228AEA861E50FFD6A228 /* HomeScreenEmptyStateView.swift in Sources */,
|
||||||
64C373ACCFA26D42BA45CFAD /* HomeScreenInvitesButton.swift in Sources */,
|
64C373ACCFA26D42BA45CFAD /* HomeScreenInvitesButton.swift in Sources */,
|
||||||
8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */,
|
8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */,
|
||||||
0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */,
|
0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */,
|
||||||
|
@ -51,14 +51,17 @@ enum HomeScreenViewAction {
|
|||||||
|
|
||||||
enum HomeScreenRoomListMode: CustomStringConvertible {
|
enum HomeScreenRoomListMode: CustomStringConvertible {
|
||||||
case skeletons
|
case skeletons
|
||||||
|
case empty
|
||||||
case rooms
|
case rooms
|
||||||
|
|
||||||
var description: String {
|
var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .rooms:
|
|
||||||
return "Showing rooms"
|
|
||||||
case .skeletons:
|
case .skeletons:
|
||||||
return "Showing placeholders"
|
return "Showing placeholders"
|
||||||
|
case .empty:
|
||||||
|
return "Showing empty state"
|
||||||
|
case .rooms:
|
||||||
|
return "Showing rooms"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
if isLoadingData {
|
if isLoadingData {
|
||||||
roomListMode = .skeletons
|
roomListMode = .skeletons
|
||||||
} else if hasNoRooms {
|
} else if hasNoRooms {
|
||||||
roomListMode = .skeletons
|
roomListMode = .empty
|
||||||
} else {
|
} else {
|
||||||
roomListMode = .rooms
|
roomListMode = .rooms
|
||||||
}
|
}
|
||||||
|
@ -31,28 +31,18 @@ struct HomeScreen: View {
|
|||||||
@State private var isSearching = false
|
@State private var isSearching = false
|
||||||
|
|
||||||
var bottomBarVisibility: Visibility {
|
var bottomBarVisibility: Visibility {
|
||||||
if lastScrollDirection == .up, context.viewState.roomListMode == .rooms {
|
switch context.viewState.roomListMode {
|
||||||
return .automatic
|
case .skeletons: return .hidden
|
||||||
} else {
|
case .empty: return .visible
|
||||||
return .hidden
|
case .rooms: return lastScrollDirection == .up ? .automatic : .hidden
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
ScrollView {
|
ScrollView {
|
||||||
if context.viewState.showSessionVerificationBanner {
|
switch context.viewState.roomListMode {
|
||||||
sessionVerificationBanner
|
case .skeletons:
|
||||||
}
|
|
||||||
|
|
||||||
if context.viewState.hasPendingInvitations, !isSearching {
|
|
||||||
HomeScreenInvitesButton(title: L10n.actionInvitesList, hasBadge: context.viewState.hasUnreadPendingInvitations) {
|
|
||||||
context.send(viewAction: .selectInvites)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
|
||||||
.padding(.vertical, -8.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if context.viewState.roomListMode == .skeletons {
|
|
||||||
LazyVStack(spacing: 0) {
|
LazyVStack(spacing: 0) {
|
||||||
ForEach(context.viewState.visibleRooms) { room in
|
ForEach(context.viewState.visibleRooms) { room in
|
||||||
HomeScreenRoomCell(room: room, context: context, isSelected: false)
|
HomeScreenRoomCell(room: room, context: context, isSelected: false)
|
||||||
@ -61,7 +51,16 @@ struct HomeScreen: View {
|
|||||||
}
|
}
|
||||||
.shimmer()
|
.shimmer()
|
||||||
.disabled(true)
|
.disabled(true)
|
||||||
} else {
|
case .empty:
|
||||||
|
HomeScreenEmptyStateLayout(minHeight: geometry.size.height) {
|
||||||
|
topSection
|
||||||
|
|
||||||
|
HomeScreenEmptyStateView(context: context)
|
||||||
|
.layoutPriority(1)
|
||||||
|
}
|
||||||
|
case .rooms:
|
||||||
|
topSection
|
||||||
|
|
||||||
LazyVStack(spacing: 0) {
|
LazyVStack(spacing: 0) {
|
||||||
HomeScreenRoomList(context: context, isSearching: $isSearching)
|
HomeScreenRoomList(context: context, isSearching: $isSearching)
|
||||||
}
|
}
|
||||||
@ -81,29 +80,24 @@ struct HomeScreen: View {
|
|||||||
updateVisibleRange()
|
updateVisibleRange()
|
||||||
}
|
}
|
||||||
.onChange(of: context.searchQuery) { searchQuery in
|
.onChange(of: context.searchQuery) { searchQuery in
|
||||||
if searchQuery.isEmpty {
|
guard searchQuery.isEmpty else { return }
|
||||||
// Allow the view to update after changing the query
|
// Dispatch allows the view to update after changing the query
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async { updateVisibleRange() }
|
||||||
updateVisibleRange()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onReceive(scrollViewAdapter.scrollDirection) { direction in
|
.onReceive(scrollViewAdapter.scrollDirection) { direction in
|
||||||
withAnimation(.elementDefault) {
|
withAnimation(.elementDefault) { lastScrollDirection = direction }
|
||||||
lastScrollDirection = direction
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onChange(of: context.viewState.visibleRooms) { _ in
|
.onChange(of: context.viewState.visibleRooms) { _ in
|
||||||
// Give the view a chance to update
|
// Dispatch gives the view a chance to update
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async { updateVisibleRange() }
|
||||||
updateVisibleRange()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.scrollDismissesKeyboard(.immediately)
|
.scrollDismissesKeyboard(.immediately)
|
||||||
.scrollDisabled(context.viewState.roomListMode == .skeletons)
|
.scrollDisabled(context.viewState.roomListMode == .skeletons)
|
||||||
|
.scrollBounceBehavior(context.viewState.roomListMode == .empty ? .basedOnSize : .automatic)
|
||||||
.animation(.elementDefault, value: context.viewState.showSessionVerificationBanner)
|
.animation(.elementDefault, value: context.viewState.showSessionVerificationBanner)
|
||||||
.animation(.elementDefault, value: context.viewState.roomListMode)
|
.animation(.elementDefault, value: context.viewState.roomListMode)
|
||||||
.animation(.none, value: context.viewState.visibleRooms)
|
.animation(.none, value: context.viewState.visibleRooms)
|
||||||
|
}
|
||||||
.alert(item: $context.alertInfo)
|
.alert(item: $context.alertInfo)
|
||||||
.alert(item: $context.leaveRoomAlertItem,
|
.alert(item: $context.leaveRoomAlertItem,
|
||||||
actions: leaveRoomAlertActions,
|
actions: leaveRoomAlertActions,
|
||||||
@ -117,23 +111,19 @@ struct HomeScreen: View {
|
|||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
@ToolbarContentBuilder
|
@ViewBuilder
|
||||||
private var toolbar: some ToolbarContent {
|
/// The session verification banner and invites button if either are needed.
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
private var topSection: some View {
|
||||||
HomeScreenUserMenuButton(context: context)
|
if context.viewState.showSessionVerificationBanner {
|
||||||
|
sessionVerificationBanner
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItemGroup(placement: .bottomBar) {
|
if context.viewState.hasPendingInvitations, !isSearching {
|
||||||
Spacer()
|
HomeScreenInvitesButton(title: L10n.actionInvitesList, hasBadge: context.viewState.hasUnreadPendingInvitations) {
|
||||||
newRoomButton
|
context.send(viewAction: .selectInvites)
|
||||||
}
|
}
|
||||||
}
|
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||||
|
.padding(.vertical, -8.0)
|
||||||
private var newRoomButton: some View {
|
|
||||||
Button {
|
|
||||||
context.send(viewAction: .startChat)
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "square.and.pencil")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,6 +163,26 @@ struct HomeScreen: View {
|
|||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ToolbarContentBuilder
|
||||||
|
private var toolbar: some ToolbarContent {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
HomeScreenUserMenuButton(context: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolbarItemGroup(placement: .bottomBar) {
|
||||||
|
Spacer()
|
||||||
|
newRoomButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var newRoomButton: some View {
|
||||||
|
Button {
|
||||||
|
context.send(viewAction: .startChat)
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "square.and.pencil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func updateVisibleRange() {
|
private func updateVisibleRange() {
|
||||||
guard let scrollView = scrollViewAdapter.scrollView,
|
guard let scrollView = scrollViewAdapter.scrollView,
|
||||||
context.viewState.visibleRooms.count > 0 else {
|
context.viewState.visibleRooms.count > 0 else {
|
||||||
@ -213,25 +223,37 @@ struct HomeScreen: View {
|
|||||||
// MARK: - Previews
|
// MARK: - Previews
|
||||||
|
|
||||||
struct HomeScreen_Previews: PreviewProvider {
|
struct HomeScreen_Previews: PreviewProvider {
|
||||||
|
static let loadingViewModel = viewModel(.loading)
|
||||||
|
static let loadedViewModel = viewModel(.loaded(.mockRooms))
|
||||||
|
static let emptyViewModel = viewModel(.loaded([]))
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
body(.loading)
|
NavigationStack {
|
||||||
body(.loaded(.mockRooms))
|
HomeScreen(context: loadingViewModel.context)
|
||||||
|
}
|
||||||
|
.previewDisplayName("Loading")
|
||||||
|
|
||||||
|
NavigationStack {
|
||||||
|
HomeScreen(context: loadedViewModel.context)
|
||||||
|
}
|
||||||
|
.previewDisplayName("Loaded")
|
||||||
|
|
||||||
|
NavigationStack {
|
||||||
|
HomeScreen(context: emptyViewModel.context)
|
||||||
|
}
|
||||||
|
.previewDisplayName("Empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func body(_ state: MockRoomSummaryProviderState) -> some View {
|
static func viewModel(_ state: MockRoomSummaryProviderState) -> HomeScreenViewModel {
|
||||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "John Doe",
|
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@alice:example.com",
|
||||||
roomSummaryProvider: MockRoomSummaryProvider(state: state)),
|
roomSummaryProvider: MockRoomSummaryProvider(state: state)),
|
||||||
mediaProvider: MockMediaProvider())
|
mediaProvider: MockMediaProvider())
|
||||||
|
|
||||||
let viewModel = HomeScreenViewModel(userSession: userSession,
|
return HomeScreenViewModel(userSession: userSession,
|
||||||
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL),
|
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL),
|
||||||
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
|
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
|
||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analytics: ServiceLocator.shared.analytics,
|
analytics: ServiceLocator.shared.analytics,
|
||||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||||
|
|
||||||
return NavigationStack {
|
|
||||||
HomeScreen(context: viewModel.context)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,163 @@
|
|||||||
|
//
|
||||||
|
// 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, systemImage: "square.and.pencil")
|
||||||
|
.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 {
|
||||||
|
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())
|
||||||
|
|
||||||
|
return HomeScreenViewModel(userSession: userSession,
|
||||||
|
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL),
|
||||||
|
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
|
analytics: ServiceLocator.shared.analytics,
|
||||||
|
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||||
|
}()
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user