diff --git a/Dangerfile.swift b/Dangerfile.swift index d53cdbcda..ec1a765de 100644 --- a/Dangerfile.swift +++ b/Dangerfile.swift @@ -50,6 +50,7 @@ let allowList = ["stefanceriu", "gileluard", "phlniji", "aringenbach", + "flescio", "Velin92"] let requiresSignOff = !allowList.contains(where: { diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index cfc6292cf..8dba8c4db 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -115,6 +115,7 @@ "all_chats" = "All Chats"; "start_chat" = "Start Chat"; "create_room" = "Create Room"; +"create_a_room" = "Create a room"; "change_space" = "Change Space"; "explore_rooms" = "Explore Rooms"; "a11y_expand_space_children" = "Expand %@ children"; @@ -1865,6 +1866,7 @@ "inviting_users_to_room" = "Inviting users…"; "invite_users_to_room_title" = "Invite Users"; "invite_friends" = "Invite friends"; +"invite_friends_to_element" = "Invite friends to Element"; "invite_friends_text" = "Hey, talk to me on %@: %@"; "invite_friends_rich_title" = "🔐️ Join me on %@"; "invitation_sent_to_one_user" = "Invitation sent to %1$@"; @@ -2355,3 +2357,4 @@ "emoji_picker_objects_category" = "Objects"; "emoji_picker_symbols_category" = "Symbols"; "emoji_picker_flags_category" = "Flags"; +"search_for_someone" = "Search for someone"; diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index 1f619ec9a..b6a96947f 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -27,6 +27,7 @@ final class AppSettings: ObservableObject { case enableInAppNotifications case pusherProfileTag case shouldCollapseRoomStateEvents + case showStartChatFlow } private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier @@ -140,7 +141,7 @@ final class AppSettings: ObservableObject { @UserSetting(key: UserDefaultsKeys.shouldCollapseRoomStateEvents.rawValue, defaultValue: true, persistIn: nil) var shouldCollapseRoomStateEvents - + // MARK: - Notifications @UserSetting(key: UserDefaultsKeys.timelineStyle.rawValue, defaultValue: true, persistIn: store) @@ -153,4 +154,11 @@ final class AppSettings: ObservableObject { // MARK: - Other let permalinkBaseURL = URL(staticString: "https://matrix.to") + + // MARK: - Feature Flags + + // MARK: Start Chat + + @UserSetting(key: UserDefaultsKeys.showStartChatFlow.rawValue, defaultValue: false, persistIn: store) + var startChatFlowFeatureFlag } diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 2384fde9c..0fde86298 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -797,6 +797,8 @@ public enum ElementL10n { public static var copiedToClipboard: String { return ElementL10n.tr("Localizable", "copied_to_clipboard") } /// Create public static var create: String { return ElementL10n.tr("Localizable", "create") } + /// Create a room + public static var createARoom: String { return ElementL10n.tr("Localizable", "create_a_room") } /// Create New Room public static var createNewRoom: String { return ElementL10n.tr("Localizable", "create_new_room") } /// Create New Space @@ -1858,6 +1860,8 @@ public enum ElementL10n { public static func inviteFriendsText(_ p1: Any, _ p2: Any) -> String { return ElementL10n.tr("Localizable", "invite_friends_text", String(describing: p1), String(describing: p2)) } + /// Invite friends to Element + public static var inviteFriendsToElement: String { return ElementL10n.tr("Localizable", "invite_friends_to_element") } /// Just to this room public static var inviteJustToThisRoom: String { return ElementL10n.tr("Localizable", "invite_just_to_this_room") } /// They won’t be a part of %@ @@ -4177,6 +4181,8 @@ public enum ElementL10n { public static var search: String { return ElementL10n.tr("Localizable", "search") } /// Filter banned users public static var searchBannedUserHint: String { return ElementL10n.tr("Localizable", "search_banned_user_hint") } + /// Search for someone + public static var searchForSomeone: String { return ElementL10n.tr("Localizable", "search_for_someone") } /// Search public static var searchHint: String { return ElementL10n.tr("Localizable", "search_hint") } /// Search Name diff --git a/ElementX/Sources/Other/AccessibilityIdentifiers.swift b/ElementX/Sources/Other/AccessibilityIdentifiers.swift index 3faf0461f..d2e1e2810 100644 --- a/ElementX/Sources/Other/AccessibilityIdentifiers.swift +++ b/ElementX/Sources/Other/AccessibilityIdentifiers.swift @@ -26,6 +26,7 @@ struct A11yIdentifiers { static let roomDetailsScreen = RoomDetailsScreen() static let sessionVerificationScreen = SessionVerificationScreen() static let softLogoutScreen = SoftLogoutScreen() + static let startChatScreen = StartChatScreen() struct BugReportScreen { let report = "bug_report-report" @@ -92,4 +93,9 @@ struct A11yIdentifiers { let clearDataMessage = "soft_logout-clear_data_message" let clearData = "soft_logout-clear_data" } + + struct StartChatScreen { + let closeStartChat = "start_chat-close" + let inviteFriends = "start_chat-invite_friends" + } } diff --git a/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index a23f3d155..45261610a 100644 --- a/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -24,8 +24,10 @@ struct DeveloperOptionsScreenViewState: BindableState { struct DeveloperOptionsScreenViewStateBindings { var shouldCollapseRoomStateEvents: Bool + var showStartChatFlow: Bool } enum DeveloperOptionsScreenViewAction { case changedShouldCollapseRoomStateEvents + case changedShowStartChatFlow } diff --git a/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift b/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift index a6e4c45ac..9d3066ed5 100644 --- a/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift @@ -22,7 +22,10 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve var callback: ((DeveloperOptionsScreenViewModelAction) -> Void)? init() { - super.init(initialViewState: DeveloperOptionsScreenViewState(bindings: DeveloperOptionsScreenViewStateBindings(shouldCollapseRoomStateEvents: ServiceLocator.shared.settings.shouldCollapseRoomStateEvents))) + let bindings = DeveloperOptionsScreenViewStateBindings(shouldCollapseRoomStateEvents: ServiceLocator.shared.settings.shouldCollapseRoomStateEvents, showStartChatFlow: ServiceLocator.shared.settings.startChatFlowFeatureFlag) + let state = DeveloperOptionsScreenViewState(bindings: bindings) + + super.init(initialViewState: state) ServiceLocator.shared.settings.$shouldCollapseRoomStateEvents .weakAssign(to: \.state.bindings.shouldCollapseRoomStateEvents, on: self) @@ -33,6 +36,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve switch viewAction { case .changedShouldCollapseRoomStateEvents: ServiceLocator.shared.settings.shouldCollapseRoomStateEvents = state.bindings.shouldCollapseRoomStateEvents + case .changedShowStartChatFlow: + ServiceLocator.shared.settings.startChatFlowFeatureFlag = state.bindings.showStartChatFlow } } } diff --git a/ElementX/Sources/Screens/DeveloperOptionsScreen/View/DeveloperOptionsScreenScreen.swift b/ElementX/Sources/Screens/DeveloperOptionsScreen/View/DeveloperOptionsScreenScreen.swift index 75750e936..c3a9ee595 100644 --- a/ElementX/Sources/Screens/DeveloperOptionsScreen/View/DeveloperOptionsScreenScreen.swift +++ b/ElementX/Sources/Screens/DeveloperOptionsScreen/View/DeveloperOptionsScreenScreen.swift @@ -29,6 +29,12 @@ struct DeveloperOptionsScreenScreen: View { .onChange(of: context.shouldCollapseRoomStateEvents) { _ in context.send(viewAction: .changedShouldCollapseRoomStateEvents) } + Toggle(isOn: $context.showStartChatFlow) { + Text("Show Start Chat flow") + } + .onChange(of: context.showStartChatFlow) { _ in + context.send(viewAction: .changedShowStartChatFlow) + } } Section { diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift index dd9d39ad7..b5c4304eb 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift @@ -29,6 +29,7 @@ enum HomeScreenCoordinatorAction { case presentSettingsScreen case presentFeedbackScreen case presentSessionVerificationScreen + case presentStartChatScreen case signOut } @@ -62,6 +63,8 @@ final class HomeScreenCoordinator: CoordinatorProtocol { self.callback?(.presentSessionVerificationScreen) case .signOut: self.callback?(.signOut) + case .presentStartChatScreen: + self.callback?(.presentStartChatScreen) } } } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index 1d3a3d396..c3f8b3f68 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -24,6 +24,7 @@ enum HomeScreenViewModelAction { case presentSettingsScreen case presentInviteFriendsScreen case presentFeedbackScreen + case presentStartChatScreen case signOut } @@ -37,6 +38,7 @@ enum HomeScreenViewUserMenuAction { enum HomeScreenViewAction { case selectRoom(roomIdentifier: String) case userMenu(action: HomeScreenViewUserMenuAction) + case startChat case verifySession case skipSessionVerification case updateVisibleItemRange(range: Range, isScrolling: Bool) @@ -67,6 +69,10 @@ struct HomeScreenViewState: BindableState { var roomListMode: HomeScreenRoomListMode = .skeletons + var showStartChatFlowEnabled: Bool { + ServiceLocator.shared.settings.startChatFlowFeatureFlag + } + 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 915ef3529..fe1e26227 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -154,6 +154,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol state.showSessionVerificationBanner = false case .updateVisibleItemRange(let range, let isScrolling): visibleItemRangePublisher.send((range, isScrolling)) + case .startChat: + callback?(.presentStartChatScreen) } } diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index 04bbf929c..2b33309ec 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -102,6 +102,12 @@ struct HomeScreen: View { ToolbarItem(placement: .navigationBarLeading) { userMenuButton } + if context.viewState.showStartChatFlowEnabled { + ToolbarItemGroup(placement: .bottomBar) { + Spacer() + newRoomButton + } + } } .background(Color.element.background.ignoresSafeArea()) } @@ -148,6 +154,12 @@ struct HomeScreen: View { .accessibilityLabel(ElementL10n.a11yAllChatsUserAvatarMenu) } + private var newRoomButton: some View { + Button(action: startChat) { + Image(systemName: "square.and.pencil") + } + } + private var sessionVerificationBanner: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 4) { @@ -191,6 +203,10 @@ struct HomeScreen: View { context.send(viewAction: .userMenu(action: .inviteFriends)) } + private func startChat() { + context.send(viewAction: .startChat) + } + private func feedback() { context.send(viewAction: .userMenu(action: .feedback)) } diff --git a/ElementX/Sources/Screens/StartChat/StartChatCoordinator.swift b/ElementX/Sources/Screens/StartChat/StartChatCoordinator.swift new file mode 100644 index 000000000..9ab8b2af4 --- /dev/null +++ b/ElementX/Sources/Screens/StartChat/StartChatCoordinator.swift @@ -0,0 +1,55 @@ +// +// 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 StartChatCoordinatorParameters { + let userSession: UserSessionProtocol +} + +enum StartChatCoordinatorAction { + case close +} + +final class StartChatCoordinator: CoordinatorProtocol { + private let parameters: StartChatCoordinatorParameters + private var viewModel: StartChatViewModelProtocol + + var callback: ((StartChatCoordinatorAction) -> Void)? + + init(parameters: StartChatCoordinatorParameters) { + self.parameters = parameters + + viewModel = StartChatViewModel(userSession: parameters.userSession) + } + + func start() { + viewModel.callback = { [weak self] action in + guard let self else { return } + switch action { + case .close: + self.callback?(.close) + case .createRoom: + // TODO: start create room flow + break + } + } + } + + func toPresentable() -> AnyView { + AnyView(StartChatScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/StartChat/StartChatModels.swift b/ElementX/Sources/Screens/StartChat/StartChatModels.swift new file mode 100644 index 000000000..14aa82880 --- /dev/null +++ b/ElementX/Sources/Screens/StartChat/StartChatModels.swift @@ -0,0 +1,39 @@ +// +// 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 StartChatViewModelAction { + case close + case createRoom +} + +struct StartChatViewState: BindableState { + var bindings = StartChatScreenViewStateBindings() + + // TODO: bind with real service, and mock data only in preview + var suggestedUsers: [RoomMemberProxy] = [.mockAlice, .mockBob, .mockCharlie] +} + +struct StartChatScreenViewStateBindings { + var searchQuery = "" +} + +enum StartChatViewAction { + case close + case createRoom + case inviteFriends +} diff --git a/ElementX/Sources/Screens/StartChat/StartChatViewModel.swift b/ElementX/Sources/Screens/StartChat/StartChatViewModel.swift new file mode 100644 index 000000000..965aeee06 --- /dev/null +++ b/ElementX/Sources/Screens/StartChat/StartChatViewModel.swift @@ -0,0 +1,44 @@ +// +// 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 + +typealias StartChatViewModelType = StateStoreViewModel + +class StartChatViewModel: StartChatViewModelType, StartChatViewModelProtocol { + private let userSession: UserSessionProtocol + + var callback: ((StartChatViewModelAction) -> Void)? + + init(userSession: UserSessionProtocol) { + self.userSession = userSession + super.init(initialViewState: StartChatViewState(), imageProvider: userSession.mediaProvider) + } + + // MARK: - Public + + override func process(viewAction: StartChatViewAction) async { + switch viewAction { + case .close: + callback?(.close) + case .createRoom: + callback?(.createRoom) + case .inviteFriends: + // TODO: start invite people flow + break + } + } +} diff --git a/ElementX/Sources/Screens/StartChat/StartChatViewModelProtocol.swift b/ElementX/Sources/Screens/StartChat/StartChatViewModelProtocol.swift new file mode 100644 index 000000000..7f538d14f --- /dev/null +++ b/ElementX/Sources/Screens/StartChat/StartChatViewModelProtocol.swift @@ -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 + +@MainActor +protocol StartChatViewModelProtocol { + var callback: ((StartChatViewModelAction) -> Void)? { get set } + var context: StartChatViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/StartChat/View/StartChatScreen.swift b/ElementX/Sources/Screens/StartChat/View/StartChatScreen.swift new file mode 100644 index 000000000..ef322e252 --- /dev/null +++ b/ElementX/Sources/Screens/StartChat/View/StartChatScreen.swift @@ -0,0 +1,102 @@ +// +// 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 StartChatScreen: View { + @ObservedObject var context: StartChatViewModel.Context + + var body: some View { + Form { + createRoomSection + inviteFriendsSection + suggestionsSection + } + .scrollContentBackground(.hidden) + .background(Color.element.formBackground.ignoresSafeArea()) + .navigationTitle(ElementL10n.startChat) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + closeButton + } + } + .searchable(text: $context.searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: ElementL10n.searchForSomeone) + } + + private var createRoomSection: some View { + Section { + Button(action: createRoom) { + Label(ElementL10n.createARoom, systemImage: "person.3") + } + .buttonStyle(FormButtonStyle(accessory: .navigationLink)) + } + .formSectionStyle() + } + + private var inviteFriendsSection: some View { + Section { + Button(action: inviteFriends) { + Label(ElementL10n.inviteFriendsToElement, systemImage: "square.and.arrow.up") + } + .buttonStyle(FormButtonStyle()) + .accessibilityIdentifier(A11yIdentifiers.startChatScreen.inviteFriends) + } + .formSectionStyle() + } + + private var suggestionsSection: some View { + Section { + ForEach(context.viewState.suggestedUsers, id: \.userId) { user in + StartChatSuggestedUserCell(user: user, imageProvider: context.imageProvider) + } + } header: { + Text(ElementL10n.directRoomUserListSuggestionsTitle) + } + .formSectionStyle() + } + + private var closeButton: some View { + Button(ElementL10n.actionCancel, action: close) + .accessibilityIdentifier(A11yIdentifiers.startChatScreen.closeStartChat) + } + + private func createRoom() { + context.send(viewAction: .createRoom) + } + + private func inviteFriends() { + context.send(viewAction: .inviteFriends) + } + + private func close() { + context.send(viewAction: .close) + } +} + +// MARK: - Previews + +struct StartChat_Previews: PreviewProvider { + static var previews: some View { + let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@userid:example.com"), + mediaProvider: MockMediaProvider()) + let regularViewModel = StartChatViewModel(userSession: userSession) + NavigationView { + StartChatScreen(context: regularViewModel.context) + .tint(.element.accent) + } + } +} diff --git a/ElementX/Sources/Screens/StartChat/View/StartChatSuggestedUserCell.swift b/ElementX/Sources/Screens/StartChat/View/StartChatSuggestedUserCell.swift new file mode 100644 index 000000000..e625b78a3 --- /dev/null +++ b/ElementX/Sources/Screens/StartChat/View/StartChatSuggestedUserCell.swift @@ -0,0 +1,47 @@ +// +// 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 StartChatSuggestedUserCell: View { + let user: RoomMemberProxy + let imageProvider: ImageProviderProtocol? + + var body: some View { + HStack(spacing: 13) { + LoadableAvatarImage(url: user.avatarURL, + name: user.displayName, + contentID: user.userId, + avatarSize: .user(on: .home), + imageProvider: imageProvider) + .accessibilityHidden(true) + + VStack(alignment: .leading, spacing: 4) { + // covers both nil and empty state + let displayName = user.displayName ?? "" + Text(displayName.isEmpty ? user.userId : displayName) + .font(.element.title3) + .foregroundColor(.element.primaryContent) + if !displayName.isEmpty { + Text(user.userId) + .font(.element.subheadline) + .foregroundColor(.element.tertiaryContent) + } + } + .accessibilityElement(children: .combine) + } + } +} diff --git a/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift b/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift index 423aff26a..5c8803ff7 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift @@ -102,6 +102,10 @@ class UserSessionFlowCoordinator: CoordinatorProtocol { case (.feedbackScreen, .dismissedFeedbackScreen, .roomList): break + case (.roomList, .showStartChatScreen, .startChatScreen): + self.presentStartChat() + case (.startChatScreen, .dismissedStartChatScreen, .roomList): + break default: fatalError("Unknown transition: \(context)") } @@ -133,6 +137,8 @@ class UserSessionFlowCoordinator: CoordinatorProtocol { self.stateMachine.processEvent(.feedbackScreen) case .presentSessionVerificationScreen: self.stateMachine.processEvent(.showSessionVerificationScreen) + case .presentStartChatScreen: + self.stateMachine.processEvent(.showStartChatScreen) case .signOut: self.callback?(.signOut) } @@ -235,6 +241,30 @@ class UserSessionFlowCoordinator: CoordinatorProtocol { self?.stateMachine.processEvent(.dismissedSessionVerificationScreen) } } + + // MARK: Start Chat + + private func presentStartChat() { + let startChatNavigationStackCoordinator = NavigationStackCoordinator() + + let userIndicatorController = UserIndicatorController(rootCoordinator: startChatNavigationStackCoordinator) + + let parameters = StartChatCoordinatorParameters(userSession: userSession) + let coordinator = StartChatCoordinator(parameters: parameters) + coordinator.callback = { [weak self] action in + guard let self else { return } + switch action { + case .close: + self.navigationSplitCoordinator.setSheetCoordinator(nil) + } + } + + startChatNavigationStackCoordinator.setRootCoordinator(coordinator) + + navigationSplitCoordinator.setSheetCoordinator(userIndicatorController) { [weak self] in + self?.stateMachine.processEvent(.dismissedStartChatScreen) + } + } // MARK: Bug reporting diff --git a/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinatorStateMachine.swift b/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinatorStateMachine.swift index fe3ea300e..780834a0d 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinatorStateMachine.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinatorStateMachine.swift @@ -34,6 +34,9 @@ class UserSessionFlowCoordinatorStateMachine { /// Showing the settings screen case settingsScreen(selectedRoomId: String?) + + /// Showing the start chat screen + case startChatScreen(selectedRoomId: String?) } /// Events that can be triggered on the AppCoordinator state machine @@ -61,6 +64,11 @@ class UserSessionFlowCoordinatorStateMachine { case showSessionVerificationScreen /// Session verification has finished case dismissedSessionVerificationScreen + + /// Request the start of the start chat flow + case showStartChatScreen + /// Start chat has been dismissed + case dismissedStartChatScreen } private let stateMachine: StateMachine @@ -74,6 +82,7 @@ class UserSessionFlowCoordinatorStateMachine { configure() } + // swiftlint:disable:next cyclomatic_complexity private func configure() { stateMachine.addRoutes(event: .start, transitions: [.initial => .roomList(selectedRoomId: nil)]) @@ -99,6 +108,10 @@ class UserSessionFlowCoordinatorStateMachine { case (.dismissedSessionVerificationScreen, .sessionVerificationScreen(let selectedRoomId)): return .roomList(selectedRoomId: selectedRoomId) + case (.showStartChatScreen, .roomList(let selectedRoomId)): + return .startChatScreen(selectedRoomId: selectedRoomId) + case (.dismissedStartChatScreen, .startChatScreen(let selectedRoomId)): + return .roomList(selectedRoomId: selectedRoomId) default: return nil } diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index d30417a84..b0c9e67b2 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -304,6 +304,11 @@ class MockScreen: Identifiable { let coordinator = ReportContentCoordinator(parameters: .init(itemID: "test", roomProxy: MockRoomProxy(displayName: "test"))) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator + case .startChat: + let navigationStackCoordinator = NavigationStackCoordinator() + let coordinator = StartChatCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"), mediaProvider: MockMediaProvider()))) + navigationStackCoordinator.setRootCoordinator(coordinator) + return navigationStackCoordinator } }() } diff --git a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift index 6f9ca28a3..aa66ef0f1 100644 --- a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift +++ b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift @@ -44,6 +44,7 @@ enum UITestsScreenIdentifier: String { case roomDetailsScreenWithRoomAvatar case roomMemberDetailsScreen case reportContent + case startChat } extension UITestsScreenIdentifier: CustomStringConvertible { diff --git a/UITests/Sources/StartChatScreenUITests.swift b/UITests/Sources/StartChatScreenUITests.swift new file mode 100644 index 000000000..1fba9b79f --- /dev/null +++ b/UITests/Sources/StartChatScreenUITests.swift @@ -0,0 +1,25 @@ +// +// 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 ElementX +import XCTest + +class StartChatScreenUITests: XCTestCase { + func testStartChatScreen() { + let app = Application.launch(.startChat) + app.assertScreenshot(.startChat) + } +} diff --git a/UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.startChat.png b/UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.startChat.png new file mode 100644 index 000000000..7b67c907f --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.startChat.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2be45f5492a94a96ff2da692738ec68d258805a297b6dec68b86fb14199a3726 +size 107619 diff --git a/UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.startChat.png b/UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.startChat.png new file mode 100644 index 000000000..1f6ed2092 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.startChat.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:153c46f827c106b49d0aeb78d242ac57027424a14899cd421ee9e77ab632310a +size 128472 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.startChat.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.startChat.png new file mode 100644 index 000000000..35fc504ae --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.startChat.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b23d1b10926ea191581b8dc38480e67e34ec0663ae8e87873356edf45f20838 +size 107029 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.startChat.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.startChat.png new file mode 100644 index 000000000..b1f12cf68 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.startChat.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e976ed0e5a41861efb0cb567b2f6311f80cf44b53c54ff453ee9a9af91f1d6a +size 126719 diff --git a/UnitTests/Sources/StartChatViewModelTests.swift b/UnitTests/Sources/StartChatViewModelTests.swift new file mode 100644 index 000000000..160afbaf9 --- /dev/null +++ b/UnitTests/Sources/StartChatViewModelTests.swift @@ -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 XCTest + +@testable import ElementX + +@MainActor +class StartChatScreenViewModelTests: XCTestCase { + var viewModel: StartChatViewModelProtocol! + var context: StartChatViewModelType.Context! + + @MainActor override func setUpWithError() throws { + let userSession = MockUserSession(clientProxy: MockClientProxy(userID: ""), + mediaProvider: MockMediaProvider()) + viewModel = StartChatViewModel(userSession: userSession) + context = viewModel.context + } +} diff --git a/changelog.d/pr-680.feature b/changelog.d/pr-680.feature new file mode 100644 index 000000000..8f753292a --- /dev/null +++ b/changelog.d/pr-680.feature @@ -0,0 +1 @@ +Add the entry point for the Start a new Chat flow, with button on home Screen and first page \ No newline at end of file