mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 13:37:11 +00:00
Epic: create and join rooms - Start Chat (#680)
* add start chat flow with UI * add feature flag for start chat * add changelog * fix naming and tests * fix empty display name in user cell * Update ElementX/Sources/Application/AppSettings.swift Co-authored-by: Alfonso Grillo <alfogrillo@element.io> * add screenshots from UI test * fix swiftFormat and add identifiers * fix warnings --------- Co-authored-by: Alfonso Grillo <alfogrillo@element.io>
This commit is contained in:
parent
7eef86970a
commit
8a069ecfa5
@ -50,6 +50,7 @@ let allowList = ["stefanceriu",
|
||||
"gileluard",
|
||||
"phlniji",
|
||||
"aringenbach",
|
||||
"flescio",
|
||||
"Velin92"]
|
||||
|
||||
let requiresSignOff = !allowList.contains(where: {
|
||||
|
@ -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";
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -24,8 +24,10 @@ struct DeveloperOptionsScreenViewState: BindableState {
|
||||
|
||||
struct DeveloperOptionsScreenViewStateBindings {
|
||||
var shouldCollapseRoomStateEvents: Bool
|
||||
var showStartChatFlow: Bool
|
||||
}
|
||||
|
||||
enum DeveloperOptionsScreenViewAction {
|
||||
case changedShouldCollapseRoomStateEvents
|
||||
case changedShowStartChatFlow
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<Int>, 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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
39
ElementX/Sources/Screens/StartChat/StartChatModels.swift
Normal file
39
ElementX/Sources/Screens/StartChat/StartChatModels.swift
Normal file
@ -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
|
||||
}
|
44
ElementX/Sources/Screens/StartChat/StartChatViewModel.swift
Normal file
44
ElementX/Sources/Screens/StartChat/StartChatViewModel.swift
Normal file
@ -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<StartChatViewState, StartChatViewAction>
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
102
ElementX/Sources/Screens/StartChat/View/StartChatScreen.swift
Normal file
102
ElementX/Sources/Screens/StartChat/View/StartChatScreen.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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<State, Event>
|
||||
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ enum UITestsScreenIdentifier: String {
|
||||
case roomDetailsScreenWithRoomAvatar
|
||||
case roomMemberDetailsScreen
|
||||
case reportContent
|
||||
case startChat
|
||||
}
|
||||
|
||||
extension UITestsScreenIdentifier: CustomStringConvertible {
|
||||
|
25
UITests/Sources/StartChatScreenUITests.swift
Normal file
25
UITests/Sources/StartChatScreenUITests.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
BIN
UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.startChat.png
(Stored with Git LFS)
Normal file
BIN
UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.startChat.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.startChat.png
(Stored with Git LFS)
Normal file
BIN
UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.startChat.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.startChat.png
(Stored with Git LFS)
Normal file
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.startChat.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.startChat.png
(Stored with Git LFS)
Normal file
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.startChat.png
(Stored with Git LFS)
Normal file
Binary file not shown.
32
UnitTests/Sources/StartChatViewModelTests.swift
Normal file
32
UnitTests/Sources/StartChatViewModelTests.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
1
changelog.d/pr-680.feature
Normal file
1
changelog.d/pr-680.feature
Normal file
@ -0,0 +1 @@
|
||||
Add the entry point for the Start a new Chat flow, with button on home Screen and first page
|
Loading…
x
Reference in New Issue
Block a user