From 863492985d0e0873bbf2e1dfd5dad0826389ecf6 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 20 Jun 2023 12:20:26 +0300 Subject: [PATCH] Various UI tweaks (#1112) * Tweak invites button frame and padding * Implement persistent selections on the room list * Stop showing rooms when focusing the room list search field with an empty query * Tweaks after code review * Improve invite button UI * Fix unit tests --- ElementX.xcodeproj/project.pbxproj | 8 ++ .../UserSessionFlowCoordinator.swift | 16 ++- .../SwiftUI/Animation/ShimmerModifier.swift | 6 +- .../HomeScreen/HomeScreenCoordinator.swift | 4 +- .../Screens/HomeScreen/HomeScreenModels.swift | 2 + .../HomeScreen/HomeScreenViewModel.swift | 8 +- .../Screens/HomeScreen/View/HomeScreen.swift | 102 +++--------------- .../View/HomeScreenInvitesButton.swift | 6 +- .../HomeScreen/View/HomeScreenRoomCell.swift | 18 ++-- .../HomeScreen/View/HomeScreenRoomList.swift | 59 ++++++++++ .../View/HomeScreenUserMenuButton.swift | 69 ++++++++++++ .../RoomScreen/RoomScreenViewModel.swift | 1 - .../UITests/UITestsAppCoordinator.swift | 3 +- .../Sources/HomeScreenViewModelTests.swift | 4 +- 14 files changed, 199 insertions(+), 107 deletions(-) create mode 100644 ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomList.swift create mode 100644 ElementX/Sources/Screens/HomeScreen/View/HomeScreenUserMenuButton.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 68c2f7c41..1c37ee8ec 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -417,6 +417,7 @@ A009BDFB0A6816D4C392ADCB /* SettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF715D4FD4710EBB637D661 /* SettingsScreenViewModelProtocol.swift */; }; A021827B528F1EDC9101CA58 /* AppCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */; }; A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */; }; + A10D6CCDE2010C09EEA1A593 /* HomeScreenRoomList.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */; }; A14A9419105A1CD42F0511C4 /* UserIndicatorModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43005941B3A2C9671E23C85 /* UserIndicatorModalView.swift */; }; A17FAD2EBC53E17B5FD384DB /* InviteUsersScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22730A30C50AC2E3D5BA8642 /* InviteUsersScreenViewModelProtocol.swift */; }; A1D4033881320C9EB88196E6 /* ServerConfirmationScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE846DDA83BFD7EC5C03760B /* ServerConfirmationScreenUITests.swift */; }; @@ -565,6 +566,7 @@ D2A15D03F81342A09340BD56 /* AnalyticsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEFEEE93B82937B2E86F92EB /* AnalyticsScreen.swift */; }; D2D70B5DB1A5E4AF0CD88330 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 033DB41C51865A2E83174E87 /* target.yml */; }; D33AC79A50DFC26D2498DD28 /* FileRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5098DA7799946A61E34A2373 /* FileRoomTimelineItem.swift */; }; + D3986615892E7CF05C86518A /* HomeScreenUserMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BC2D3573D900A9C9F8C191 /* HomeScreenUserMenuButton.swift */; }; D3FD96913D2B1AAA3149DAC7 /* CreateRoomViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69D42EE0102D2857933625DD /* CreateRoomViewModelTests.swift */; }; D415764645491F10344FC6AC /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60F18AECC9D38C2B6D85F99C /* Publisher.swift */; }; D43F0503EF2CBC55272538FE /* SDKGeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2F079B5DBD0D85FEA687AAE /* SDKGeneratedMocks.swift */; }; @@ -1210,6 +1212,7 @@ C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAvatarImage.swift; sourceTree = ""; }; C733D11B421CFE3A657EF230 /* test_image.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = test_image.png; sourceTree = ""; }; C75EF87651B00A176AB08E97 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomList.swift; sourceTree = ""; }; C789E7BFC066CF39B8AE0974 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = ""; }; C830A64609CBD152F06E0457 /* NotificationConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationConstants.swift; sourceTree = ""; }; C843CF833BF6485B64AC87E1 /* AppRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouter.swift; sourceTree = ""; }; @@ -1316,6 +1319,7 @@ F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = ""; }; F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = ""; }; F36C0A6D59717193F49EA986 /* UserSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionTests.swift; sourceTree = ""; }; + F3BC2D3573D900A9C9F8C191 /* HomeScreenUserMenuButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenUserMenuButton.swift; sourceTree = ""; }; F3EAE3E9D5EF4A6D5D9C6CFD /* EmojiPickerScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenViewModel.swift; sourceTree = ""; }; F4548A9BDE5CB3AB864BCA9F /* EffectsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EffectsView.swift; sourceTree = ""; }; F506C6ADB1E1DA6638078E11 /* UITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2008,6 +2012,8 @@ B902EA6CD3296B0E10EE432B /* HomeScreen.swift */, 24227FF9A2797F6EA7F69CDD /* HomeScreenInvitesButton.swift */, ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */, + C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */, + F3BC2D3573D900A9C9F8C191 /* HomeScreenUserMenuButton.swift */, ); path = View; sourceTree = ""; @@ -3979,6 +3985,8 @@ 64C373ACCFA26D42BA45CFAD /* HomeScreenInvitesButton.swift in Sources */, 8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */, 0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */, + A10D6CCDE2010C09EEA1A593 /* HomeScreenRoomList.swift in Sources */, + D3986615892E7CF05C86518A /* HomeScreenUserMenuButton.swift in Sources */, DE4F8C4E0F1DB4832F09DE97 /* HomeScreenViewModel.swift in Sources */, 56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */, 03D684A3AE85A23B3DA3B43F /* Image.swift in Sources */, diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index 2c72f76d7..807bac449 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -36,6 +36,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { private let sidebarNavigationStackCoordinator: NavigationStackCoordinator private let detailNavigationStackCoordinator: NavigationStackCoordinator + private let selectedRoomSubject = CurrentValueSubject(nil) + var callback: ((UserSessionFlowCoordinatorAction) -> Void)? init(userSession: UserSessionProtocol, @@ -109,7 +111,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { // MARK: - Private - // swiftlint:disable:next cyclomatic_complexity + // swiftlint:disable:next cyclomatic_complexity function_body_length private func setupStateMachine() { stateMachine.addTransitionHandler { [weak self] context in guard let self else { return } @@ -160,6 +162,15 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { } } + stateMachine.addTransitionHandler { [weak self] context in + switch context.toState { + case .roomList(let selectedRoomId): + self?.selectedRoomSubject.send(selectedRoomId) + default: + break + } + } + stateMachine.addErrorHandler { context in fatalError("Failed transition with context: \(context)") } @@ -170,7 +181,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { let parameters = HomeScreenCoordinatorParameters(userSession: userSession, attributedStringBuilder: AttributedStringBuilder(), bugReportService: bugReportService, - navigationStackCoordinator: detailNavigationStackCoordinator) + navigationStackCoordinator: detailNavigationStackCoordinator, + selectedRoomPublisher: selectedRoomSubject.asCurrentValuePublisher()) let coordinator = HomeScreenCoordinator(parameters: parameters) coordinator.callback = { [weak self] action in diff --git a/ElementX/Sources/Other/SwiftUI/Animation/ShimmerModifier.swift b/ElementX/Sources/Other/SwiftUI/Animation/ShimmerModifier.swift index 015b85a14..1b29fa4e3 100644 --- a/ElementX/Sources/Other/SwiftUI/Animation/ShimmerModifier.swift +++ b/ElementX/Sources/Other/SwiftUI/Animation/ShimmerModifier.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import SwiftUI /// A view modifier that applies a shimmering effect to the view. @@ -72,12 +73,13 @@ extension View { struct ShimmerOverlay_Previews: PreviewProvider { static let viewModel = HomeScreenViewModel(userSession: MockUserSession(clientProxy: MockClientProxy(userID: ""), mediaProvider: MockMediaProvider()), - attributedStringBuilder: AttributedStringBuilder()) + attributedStringBuilder: AttributedStringBuilder(), + selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher()) static var previews: some View { VStack { ForEach(0...8, id: \.self) { _ in - HomeScreenRoomCell(room: .placeholder(), context: viewModel.context) + HomeScreenRoomCell(room: .placeholder(), context: viewModel.context, isSelected: false) } } .redacted(reason: .placeholder) diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift index e28ae861d..fa6c8b637 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift @@ -22,6 +22,7 @@ struct HomeScreenCoordinatorParameters { let attributedStringBuilder: AttributedStringBuilderProtocol let bugReportService: BugReportServiceProtocol let navigationStackCoordinator: NavigationStackCoordinator + let selectedRoomPublisher: CurrentValuePublisher } enum HomeScreenCoordinatorAction { @@ -48,7 +49,8 @@ final class HomeScreenCoordinator: CoordinatorProtocol { self.parameters = parameters viewModel = HomeScreenViewModel(userSession: parameters.userSession, - attributedStringBuilder: parameters.attributedStringBuilder) + attributedStringBuilder: parameters.attributedStringBuilder, + selectedRoomPublisher: parameters.selectedRoomPublisher) viewModel.callback = { [weak self] action in guard let self else { return } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index 6505e209a..84d5a1b09 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -74,6 +74,8 @@ struct HomeScreenViewState: BindableState { var hasPendingInvitations = false var hasUnreadPendingInvitations = false + var selectedRoomID: String? + var visibleRooms: [HomeScreenRoom] { if roomListMode == .skeletons { return placeholderRooms diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 18dfd8862..cac5c2ac5 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -31,7 +31,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol var callback: ((HomeScreenViewModelAction) -> Void)? // swiftlint:disable:next function_body_length cyclomatic_complexity - init(userSession: UserSessionProtocol, attributedStringBuilder: AttributedStringBuilderProtocol) { + init(userSession: UserSessionProtocol, + attributedStringBuilder: AttributedStringBuilderProtocol, + selectedRoomPublisher: CurrentValuePublisher) { self.userSession = userSession self.attributedStringBuilder = attributedStringBuilder @@ -59,6 +61,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol .weakAssign(to: \.state.userAvatarURL, on: self) .store(in: &cancellables) + selectedRoomPublisher + .weakAssign(to: \.state.selectedRoomID, on: self) + .store(in: &cancellables) + guard let roomSummaryProvider, let inviteSummaryProvider else { MXLog.error("Room summary provider unavailable") return diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index 56c1697cf..dd534a293 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import Compound import SwiftUI @@ -25,27 +26,27 @@ struct HomeScreen: View { @ObservedObject var context: HomeScreenViewModel.Context @State private var scrollViewAdapter = ScrollViewAdapter() - @State private var showingLogoutConfirmation = false @State private var showingBottomToolbar = true + @State private var isSearching = false var body: some View { ScrollView { if context.viewState.showSessionVerificationBanner { sessionVerificationBanner } - - if context.viewState.hasPendingInvitations { + + if context.viewState.hasPendingInvitations, !isSearching { HomeScreenInvitesButton(title: L10n.actionInvitesList, hasBadge: context.viewState.hasUnreadPendingInvitations) { context.send(viewAction: .selectInvites) } - .padding(.trailing, 16) .frame(maxWidth: .infinity, alignment: .trailing) + .padding(.vertical, -8.0) } if context.viewState.roomListMode == .skeletons { LazyVStack(spacing: 0) { ForEach(context.viewState.visibleRooms) { room in - HomeScreenRoomCell(room: room, context: context) + HomeScreenRoomCell(room: room, context: context, isSelected: false) .redacted(reason: .placeholder) } } @@ -53,29 +54,7 @@ struct HomeScreen: View { .disabled(true) } else { LazyVStack(spacing: 0) { - ForEach(context.viewState.visibleRooms) { room in - Group { - if room.isPlaceholder { - HomeScreenRoomCell(room: room, context: context) - .redacted(reason: .placeholder) - } else { - HomeScreenRoomCell(room: room, context: context) - .contextMenu { - Button { - context.send(viewAction: .showRoomDetails(roomIdentifier: room.id)) - } label: { - Label(L10n.commonSettings, systemImage: "gearshape") - } - - Button(role: .destructive) { - context.send(viewAction: .leaveRoom(roomIdentifier: room.id)) - } label: { - Label(L10n.actionLeaveRoom, systemImage: "rectangle.portrait.and.arrow.right") - } - } - } - } - } + HomeScreenRoomList(context: context, isSearching: $isSearching) } .searchable(text: $context.searchQuery) .compoundSearchField() @@ -132,7 +111,7 @@ struct HomeScreen: View { @ToolbarContentBuilder private var toolbar: some ToolbarContent { ToolbarItem(placement: .navigationBarLeading) { - userMenuButton + HomeScreenUserMenuButton(context: context) } ToolbarItemGroup(placement: .bottomBar) { @@ -140,51 +119,11 @@ struct HomeScreen: View { newRoomButton } } - - @ViewBuilder - private var userMenuButton: some View { - Menu { - Section { - Button(action: settings) { - Label(L10n.commonSettings, systemImage: "gearshape") - } - } - Section { - MatrixUserShareLink(userID: context.viewState.userID) { - Label(L10n.actionInvite, systemImage: "square.and.arrow.up") - } - Button(action: feedback) { - Label(L10n.commonReportABug, systemImage: "ladybug") - } - } - Section { - Button(role: .destructive) { - showingLogoutConfirmation = true - } label: { - Label(L10n.screenSignoutPreferenceItem, systemImage: "rectangle.portrait.and.arrow.right") - } - } - } label: { - LoadableAvatarImage(url: context.viewState.userAvatarURL, - name: context.viewState.userDisplayName, - contentID: context.viewState.userID, - avatarSize: .user(on: .home), - imageProvider: context.imageProvider) - .accessibilityIdentifier(A11yIdentifiers.homeScreen.userAvatar) - } - .alert(L10n.screenSignoutConfirmationDialogTitle, - isPresented: $showingLogoutConfirmation) { - Button(L10n.screenSignoutConfirmationDialogSubmit, - role: .destructive, - action: signOut) - } message: { - Text(L10n.screenSignoutConfirmationDialogContent) - } - .accessibilityLabel(L10n.a11yUserMenu) - } private var newRoomButton: some View { - Button(action: startChat) { + Button { + context.send(viewAction: .startChat) + } label: { Image(systemName: "square.and.pencil") } } @@ -224,22 +163,6 @@ struct HomeScreen: View { .cornerRadius(14) .padding(.horizontal, 16) } - - private func settings() { - context.send(viewAction: .userMenu(action: .settings)) - } - - private func startChat() { - context.send(viewAction: .startChat) - } - - private func feedback() { - context.send(viewAction: .userMenu(action: .feedback)) - } - - private func signOut() { - context.send(viewAction: .userMenu(action: .signOut)) - } private func updateVisibleRange() { guard let scrollView = scrollViewAdapter.scrollView, @@ -292,7 +215,8 @@ struct HomeScreen_Previews: PreviewProvider { mediaProvider: MockMediaProvider()) let viewModel = HomeScreenViewModel(userSession: userSession, - attributedStringBuilder: AttributedStringBuilder()) + attributedStringBuilder: AttributedStringBuilder(), + selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher()) return NavigationStack { HomeScreen(context: viewModel.context) diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInvitesButton.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInvitesButton.swift index 8cc57b98f..7bf8be3c4 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInvitesButton.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInvitesButton.swift @@ -25,7 +25,7 @@ struct HomeScreenInvitesButton: View { var body: some View { Button(action: action) { - HStack(spacing: 8) { + HStack(spacing: 8.0) { Text(title) .foregroundColor(.compound.textPrimary) .font(.compound.bodyMDSemibold) @@ -34,7 +34,9 @@ struct HomeScreenInvitesButton: View { badge } } - .padding(.vertical, 4) + .padding(.trailing, 16.0) + .padding(.leading, 64.0) + .padding(.vertical, 8.0) } } diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift index abcf932e2..811058e87 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import SwiftUI struct HomeScreenRoomCell: View { @@ -22,6 +23,7 @@ struct HomeScreenRoomCell: View { let room: HomeScreenRoom let context: HomeScreenViewModel.Context + let isSelected: Bool private let verticalInsets = 12.0 private let horizontalInsets = 16.0 @@ -47,7 +49,7 @@ struct HomeScreenRoomCell: View { .padding(.horizontal, horizontalInsets) .accessibilityElement(children: .combine) } - .buttonStyle(HomeScreenRoomCellButtonStyle(isSelected: false)) + .buttonStyle(HomeScreenRoomCellButtonStyle(isSelected: isSelected)) .accessibilityIdentifier(A11yIdentifiers.homeScreen.roomName(room.name)) } @@ -149,8 +151,9 @@ struct HomeScreenRoomCellButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label - .background(configuration.isPressed || isSelected ? Color.compound.bgSubtleSecondary : .clear) + .background(isSelected ? Color.compound.bgSubtleSecondary : Color.compound.bgCanvasDefault) .contentShape(Rectangle()) + .animation(.elementDefault, value: isSelected) } } @@ -171,7 +174,8 @@ struct HomeScreenRoomCell_Previews: PreviewProvider { mediaProvider: MockMediaProvider()) let viewModel = HomeScreenViewModel(userSession: userSession, - attributedStringBuilder: AttributedStringBuilder()) + attributedStringBuilder: AttributedStringBuilder(), + selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher()) let rooms: [HomeScreenRoom] = summaryProvider.roomListPublisher.value.compactMap { summary -> HomeScreenRoom? in switch summary { @@ -190,14 +194,14 @@ struct HomeScreenRoomCell_Previews: PreviewProvider { return VStack(spacing: 0) { ForEach(rooms) { room in - HomeScreenRoomCell(room: room, context: viewModel.context) + HomeScreenRoomCell(room: room, context: viewModel.context, isSelected: false) } - HomeScreenRoomCell(room: .placeholder(), context: viewModel.context) + HomeScreenRoomCell(room: .placeholder(), context: viewModel.context, isSelected: false) .redacted(reason: .placeholder) - HomeScreenRoomCell(room: .placeholder(), context: viewModel.context) + HomeScreenRoomCell(room: .placeholder(), context: viewModel.context, isSelected: false) .redacted(reason: .placeholder) - HomeScreenRoomCell(room: .placeholder(), context: viewModel.context) + HomeScreenRoomCell(room: .placeholder(), context: viewModel.context, isSelected: false) .redacted(reason: .placeholder) } } diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomList.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomList.swift new file mode 100644 index 000000000..c8ba1abea --- /dev/null +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomList.swift @@ -0,0 +1,59 @@ +// +// 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 + +struct HomeScreenRoomList: View { + @Environment(\.isSearching) var isSearchFieldFocused + + @ObservedObject var context: HomeScreenViewModel.Context + @Binding var isSearching: Bool + + var body: some View { + content + .onChange(of: isSearchFieldFocused) { isSearching = $0 } + } + + @ViewBuilder + private var content: some View { + if isSearchFieldFocused, context.searchQuery.count == 0 { + EmptyView() + } else { + ForEach(context.viewState.visibleRooms) { room in + if room.isPlaceholder { + HomeScreenRoomCell(room: room, context: context, isSelected: false) + .redacted(reason: .placeholder) + } else { + let isSelected = context.viewState.selectedRoomID == room.id + HomeScreenRoomCell(room: room, context: context, isSelected: isSelected) + .contextMenu { + Button { + context.send(viewAction: .showRoomDetails(roomIdentifier: room.id)) + } label: { + Label(L10n.commonSettings, systemImage: "gearshape") + } + + Button(role: .destructive) { + context.send(viewAction: .leaveRoom(roomIdentifier: room.id)) + } label: { + Label(L10n.actionLeaveRoom, systemImage: "rectangle.portrait.and.arrow.right") + } + } + } + } + } + } +} diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenUserMenuButton.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenUserMenuButton.swift new file mode 100644 index 000000000..99faf9e6e --- /dev/null +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenUserMenuButton.swift @@ -0,0 +1,69 @@ +// +// 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 + +struct HomeScreenUserMenuButton: View { + @State private var showingLogoutConfirmation = false + + let context: HomeScreenViewModel.Context + + var body: some View { + Menu { + Section { + Button { + context.send(viewAction: .userMenu(action: .settings)) + } label: { + Label(L10n.commonSettings, systemImage: "gearshape") + } + } + Section { + MatrixUserShareLink(userID: context.viewState.userID) { + Label(L10n.actionInvite, systemImage: "square.and.arrow.up") + } + Button { + context.send(viewAction: .userMenu(action: .feedback)) + } label: { + Label(L10n.commonReportABug, systemImage: "ladybug") + } + } + Section { + Button(role: .destructive) { + showingLogoutConfirmation = true + } label: { + Label(L10n.screenSignoutPreferenceItem, systemImage: "rectangle.portrait.and.arrow.right") + } + } + } label: { + LoadableAvatarImage(url: context.viewState.userAvatarURL, + name: context.viewState.userDisplayName, + contentID: context.viewState.userID, + avatarSize: .user(on: .home), + imageProvider: context.imageProvider) + .accessibilityIdentifier(A11yIdentifiers.homeScreen.userAvatar) + } + .alert(L10n.screenSignoutConfirmationDialogTitle, + isPresented: $showingLogoutConfirmation) { + Button(L10n.screenSignoutConfirmationDialogSubmit, + role: .destructive) { + context.send(viewAction: .userMenu(action: .signOut)) + } + } message: { + Text(L10n.screenSignoutConfirmationDialogContent) + } + .accessibilityLabel(L10n.a11yUserMenu) + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index f1871b664..a0ac414e7 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -117,7 +117,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol // MARK: - Private - // swiftlint:disable:next function_body_length private func setupSubscriptions() { timelineController.callbacks .receive(on: DispatchQueue.main) diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 4d76da8d2..1b03a01d0 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -134,7 +134,8 @@ class MockScreen: Identifiable { let coordinator = HomeScreenCoordinator(parameters: .init(userSession: session, attributedStringBuilder: AttributedStringBuilder(), bugReportService: BugReportServiceMock(), - navigationStackCoordinator: navigationStackCoordinator)) + navigationStackCoordinator: navigationStackCoordinator, + selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher())) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .settings: diff --git a/UnitTests/Sources/HomeScreenViewModelTests.swift b/UnitTests/Sources/HomeScreenViewModelTests.swift index bafcb7b93..4b95fdced 100644 --- a/UnitTests/Sources/HomeScreenViewModelTests.swift +++ b/UnitTests/Sources/HomeScreenViewModelTests.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import XCTest @testable import ElementX @@ -31,7 +32,8 @@ class HomeScreenViewModelTests: XCTestCase { clientProxy = MockClientProxy(userID: "@mock:client.com") viewModel = HomeScreenViewModel(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider()), - attributedStringBuilder: AttributedStringBuilder()) + attributedStringBuilder: AttributedStringBuilder(), + selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher()) } func testSelectRoom() async throws {