mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 00:49:44 +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:
@@ -50,6 +50,7 @@ let allowList = ["stefanceriu",
|
|||||||
"gileluard",
|
"gileluard",
|
||||||
"phlniji",
|
"phlniji",
|
||||||
"aringenbach",
|
"aringenbach",
|
||||||
|
"flescio",
|
||||||
"Velin92"]
|
"Velin92"]
|
||||||
|
|
||||||
let requiresSignOff = !allowList.contains(where: {
|
let requiresSignOff = !allowList.contains(where: {
|
||||||
|
|||||||
@@ -115,6 +115,7 @@
|
|||||||
"all_chats" = "All Chats";
|
"all_chats" = "All Chats";
|
||||||
"start_chat" = "Start Chat";
|
"start_chat" = "Start Chat";
|
||||||
"create_room" = "Create Room";
|
"create_room" = "Create Room";
|
||||||
|
"create_a_room" = "Create a room";
|
||||||
"change_space" = "Change Space";
|
"change_space" = "Change Space";
|
||||||
"explore_rooms" = "Explore Rooms";
|
"explore_rooms" = "Explore Rooms";
|
||||||
"a11y_expand_space_children" = "Expand %@ children";
|
"a11y_expand_space_children" = "Expand %@ children";
|
||||||
@@ -1865,6 +1866,7 @@
|
|||||||
"inviting_users_to_room" = "Inviting users…";
|
"inviting_users_to_room" = "Inviting users…";
|
||||||
"invite_users_to_room_title" = "Invite Users";
|
"invite_users_to_room_title" = "Invite Users";
|
||||||
"invite_friends" = "Invite friends";
|
"invite_friends" = "Invite friends";
|
||||||
|
"invite_friends_to_element" = "Invite friends to Element";
|
||||||
"invite_friends_text" = "Hey, talk to me on %@: %@";
|
"invite_friends_text" = "Hey, talk to me on %@: %@";
|
||||||
"invite_friends_rich_title" = "🔐️ Join me on %@";
|
"invite_friends_rich_title" = "🔐️ Join me on %@";
|
||||||
"invitation_sent_to_one_user" = "Invitation sent to %1$@";
|
"invitation_sent_to_one_user" = "Invitation sent to %1$@";
|
||||||
@@ -2355,3 +2357,4 @@
|
|||||||
"emoji_picker_objects_category" = "Objects";
|
"emoji_picker_objects_category" = "Objects";
|
||||||
"emoji_picker_symbols_category" = "Symbols";
|
"emoji_picker_symbols_category" = "Symbols";
|
||||||
"emoji_picker_flags_category" = "Flags";
|
"emoji_picker_flags_category" = "Flags";
|
||||||
|
"search_for_someone" = "Search for someone";
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ final class AppSettings: ObservableObject {
|
|||||||
case enableInAppNotifications
|
case enableInAppNotifications
|
||||||
case pusherProfileTag
|
case pusherProfileTag
|
||||||
case shouldCollapseRoomStateEvents
|
case shouldCollapseRoomStateEvents
|
||||||
|
case showStartChatFlow
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
||||||
@@ -153,4 +154,11 @@ final class AppSettings: ObservableObject {
|
|||||||
// MARK: - Other
|
// MARK: - Other
|
||||||
|
|
||||||
let permalinkBaseURL = URL(staticString: "https://matrix.to")
|
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") }
|
public static var copiedToClipboard: String { return ElementL10n.tr("Localizable", "copied_to_clipboard") }
|
||||||
/// Create
|
/// Create
|
||||||
public static var create: String { return ElementL10n.tr("Localizable", "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
|
/// Create New Room
|
||||||
public static var createNewRoom: String { return ElementL10n.tr("Localizable", "create_new_room") }
|
public static var createNewRoom: String { return ElementL10n.tr("Localizable", "create_new_room") }
|
||||||
/// Create New Space
|
/// Create New Space
|
||||||
@@ -1858,6 +1860,8 @@ public enum ElementL10n {
|
|||||||
public static func inviteFriendsText(_ p1: Any, _ p2: Any) -> String {
|
public static func inviteFriendsText(_ p1: Any, _ p2: Any) -> String {
|
||||||
return ElementL10n.tr("Localizable", "invite_friends_text", String(describing: p1), String(describing: p2))
|
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
|
/// Just to this room
|
||||||
public static var inviteJustToThisRoom: String { return ElementL10n.tr("Localizable", "invite_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 %@
|
/// They won’t be a part of %@
|
||||||
@@ -4177,6 +4181,8 @@ public enum ElementL10n {
|
|||||||
public static var search: String { return ElementL10n.tr("Localizable", "search") }
|
public static var search: String { return ElementL10n.tr("Localizable", "search") }
|
||||||
/// Filter banned users
|
/// Filter banned users
|
||||||
public static var searchBannedUserHint: String { return ElementL10n.tr("Localizable", "search_banned_user_hint") }
|
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
|
/// Search
|
||||||
public static var searchHint: String { return ElementL10n.tr("Localizable", "search_hint") }
|
public static var searchHint: String { return ElementL10n.tr("Localizable", "search_hint") }
|
||||||
/// Search Name
|
/// Search Name
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ struct A11yIdentifiers {
|
|||||||
static let roomDetailsScreen = RoomDetailsScreen()
|
static let roomDetailsScreen = RoomDetailsScreen()
|
||||||
static let sessionVerificationScreen = SessionVerificationScreen()
|
static let sessionVerificationScreen = SessionVerificationScreen()
|
||||||
static let softLogoutScreen = SoftLogoutScreen()
|
static let softLogoutScreen = SoftLogoutScreen()
|
||||||
|
static let startChatScreen = StartChatScreen()
|
||||||
|
|
||||||
struct BugReportScreen {
|
struct BugReportScreen {
|
||||||
let report = "bug_report-report"
|
let report = "bug_report-report"
|
||||||
@@ -92,4 +93,9 @@ struct A11yIdentifiers {
|
|||||||
let clearDataMessage = "soft_logout-clear_data_message"
|
let clearDataMessage = "soft_logout-clear_data_message"
|
||||||
let clearData = "soft_logout-clear_data"
|
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 {
|
struct DeveloperOptionsScreenViewStateBindings {
|
||||||
var shouldCollapseRoomStateEvents: Bool
|
var shouldCollapseRoomStateEvents: Bool
|
||||||
|
var showStartChatFlow: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DeveloperOptionsScreenViewAction {
|
enum DeveloperOptionsScreenViewAction {
|
||||||
case changedShouldCollapseRoomStateEvents
|
case changedShouldCollapseRoomStateEvents
|
||||||
|
case changedShowStartChatFlow
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -22,7 +22,10 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve
|
|||||||
var callback: ((DeveloperOptionsScreenViewModelAction) -> Void)?
|
var callback: ((DeveloperOptionsScreenViewModelAction) -> Void)?
|
||||||
|
|
||||||
init() {
|
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
|
ServiceLocator.shared.settings.$shouldCollapseRoomStateEvents
|
||||||
.weakAssign(to: \.state.bindings.shouldCollapseRoomStateEvents, on: self)
|
.weakAssign(to: \.state.bindings.shouldCollapseRoomStateEvents, on: self)
|
||||||
@@ -33,6 +36,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve
|
|||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .changedShouldCollapseRoomStateEvents:
|
case .changedShouldCollapseRoomStateEvents:
|
||||||
ServiceLocator.shared.settings.shouldCollapseRoomStateEvents = state.bindings.shouldCollapseRoomStateEvents
|
ServiceLocator.shared.settings.shouldCollapseRoomStateEvents = state.bindings.shouldCollapseRoomStateEvents
|
||||||
|
case .changedShowStartChatFlow:
|
||||||
|
ServiceLocator.shared.settings.startChatFlowFeatureFlag = state.bindings.showStartChatFlow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -29,6 +29,12 @@ struct DeveloperOptionsScreenScreen: View {
|
|||||||
.onChange(of: context.shouldCollapseRoomStateEvents) { _ in
|
.onChange(of: context.shouldCollapseRoomStateEvents) { _ in
|
||||||
context.send(viewAction: .changedShouldCollapseRoomStateEvents)
|
context.send(viewAction: .changedShouldCollapseRoomStateEvents)
|
||||||
}
|
}
|
||||||
|
Toggle(isOn: $context.showStartChatFlow) {
|
||||||
|
Text("Show Start Chat flow")
|
||||||
|
}
|
||||||
|
.onChange(of: context.showStartChatFlow) { _ in
|
||||||
|
context.send(viewAction: .changedShowStartChatFlow)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ enum HomeScreenCoordinatorAction {
|
|||||||
case presentSettingsScreen
|
case presentSettingsScreen
|
||||||
case presentFeedbackScreen
|
case presentFeedbackScreen
|
||||||
case presentSessionVerificationScreen
|
case presentSessionVerificationScreen
|
||||||
|
case presentStartChatScreen
|
||||||
case signOut
|
case signOut
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +63,8 @@ final class HomeScreenCoordinator: CoordinatorProtocol {
|
|||||||
self.callback?(.presentSessionVerificationScreen)
|
self.callback?(.presentSessionVerificationScreen)
|
||||||
case .signOut:
|
case .signOut:
|
||||||
self.callback?(.signOut)
|
self.callback?(.signOut)
|
||||||
|
case .presentStartChatScreen:
|
||||||
|
self.callback?(.presentStartChatScreen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ enum HomeScreenViewModelAction {
|
|||||||
case presentSettingsScreen
|
case presentSettingsScreen
|
||||||
case presentInviteFriendsScreen
|
case presentInviteFriendsScreen
|
||||||
case presentFeedbackScreen
|
case presentFeedbackScreen
|
||||||
|
case presentStartChatScreen
|
||||||
case signOut
|
case signOut
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ enum HomeScreenViewUserMenuAction {
|
|||||||
enum HomeScreenViewAction {
|
enum HomeScreenViewAction {
|
||||||
case selectRoom(roomIdentifier: String)
|
case selectRoom(roomIdentifier: String)
|
||||||
case userMenu(action: HomeScreenViewUserMenuAction)
|
case userMenu(action: HomeScreenViewUserMenuAction)
|
||||||
|
case startChat
|
||||||
case verifySession
|
case verifySession
|
||||||
case skipSessionVerification
|
case skipSessionVerification
|
||||||
case updateVisibleItemRange(range: Range<Int>, isScrolling: Bool)
|
case updateVisibleItemRange(range: Range<Int>, isScrolling: Bool)
|
||||||
@@ -67,6 +69,10 @@ struct HomeScreenViewState: BindableState {
|
|||||||
|
|
||||||
var roomListMode: HomeScreenRoomListMode = .skeletons
|
var roomListMode: HomeScreenRoomListMode = .skeletons
|
||||||
|
|
||||||
|
var showStartChatFlowEnabled: Bool {
|
||||||
|
ServiceLocator.shared.settings.startChatFlowFeatureFlag
|
||||||
|
}
|
||||||
|
|
||||||
var visibleRooms: [HomeScreenRoom] {
|
var visibleRooms: [HomeScreenRoom] {
|
||||||
if roomListMode == .skeletons {
|
if roomListMode == .skeletons {
|
||||||
return placeholderRooms
|
return placeholderRooms
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
state.showSessionVerificationBanner = false
|
state.showSessionVerificationBanner = false
|
||||||
case .updateVisibleItemRange(let range, let isScrolling):
|
case .updateVisibleItemRange(let range, let isScrolling):
|
||||||
visibleItemRangePublisher.send((range, isScrolling))
|
visibleItemRangePublisher.send((range, isScrolling))
|
||||||
|
case .startChat:
|
||||||
|
callback?(.presentStartChatScreen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,12 @@ struct HomeScreen: View {
|
|||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
userMenuButton
|
userMenuButton
|
||||||
}
|
}
|
||||||
|
if context.viewState.showStartChatFlowEnabled {
|
||||||
|
ToolbarItemGroup(placement: .bottomBar) {
|
||||||
|
Spacer()
|
||||||
|
newRoomButton
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.background(Color.element.background.ignoresSafeArea())
|
.background(Color.element.background.ignoresSafeArea())
|
||||||
}
|
}
|
||||||
@@ -148,6 +154,12 @@ struct HomeScreen: View {
|
|||||||
.accessibilityLabel(ElementL10n.a11yAllChatsUserAvatarMenu)
|
.accessibilityLabel(ElementL10n.a11yAllChatsUserAvatarMenu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var newRoomButton: some View {
|
||||||
|
Button(action: startChat) {
|
||||||
|
Image(systemName: "square.and.pencil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var sessionVerificationBanner: some View {
|
private var sessionVerificationBanner: some View {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
@@ -191,6 +203,10 @@ struct HomeScreen: View {
|
|||||||
context.send(viewAction: .userMenu(action: .inviteFriends))
|
context.send(viewAction: .userMenu(action: .inviteFriends))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func startChat() {
|
||||||
|
context.send(viewAction: .startChat)
|
||||||
|
}
|
||||||
|
|
||||||
private func feedback() {
|
private func feedback() {
|
||||||
context.send(viewAction: .userMenu(action: .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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
}
|
||||||
@@ -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):
|
case (.feedbackScreen, .dismissedFeedbackScreen, .roomList):
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case (.roomList, .showStartChatScreen, .startChatScreen):
|
||||||
|
self.presentStartChat()
|
||||||
|
case (.startChatScreen, .dismissedStartChatScreen, .roomList):
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
fatalError("Unknown transition: \(context)")
|
fatalError("Unknown transition: \(context)")
|
||||||
}
|
}
|
||||||
@@ -133,6 +137,8 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
|||||||
self.stateMachine.processEvent(.feedbackScreen)
|
self.stateMachine.processEvent(.feedbackScreen)
|
||||||
case .presentSessionVerificationScreen:
|
case .presentSessionVerificationScreen:
|
||||||
self.stateMachine.processEvent(.showSessionVerificationScreen)
|
self.stateMachine.processEvent(.showSessionVerificationScreen)
|
||||||
|
case .presentStartChatScreen:
|
||||||
|
self.stateMachine.processEvent(.showStartChatScreen)
|
||||||
case .signOut:
|
case .signOut:
|
||||||
self.callback?(.signOut)
|
self.callback?(.signOut)
|
||||||
}
|
}
|
||||||
@@ -236,6 +242,30 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// MARK: Bug reporting
|
||||||
|
|
||||||
private func presentFeedbackScreen(for image: UIImage? = nil) {
|
private func presentFeedbackScreen(for image: UIImage? = nil) {
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ class UserSessionFlowCoordinatorStateMachine {
|
|||||||
|
|
||||||
/// Showing the settings screen
|
/// Showing the settings screen
|
||||||
case settingsScreen(selectedRoomId: String?)
|
case settingsScreen(selectedRoomId: String?)
|
||||||
|
|
||||||
|
/// Showing the start chat screen
|
||||||
|
case startChatScreen(selectedRoomId: String?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Events that can be triggered on the AppCoordinator state machine
|
/// Events that can be triggered on the AppCoordinator state machine
|
||||||
@@ -61,6 +64,11 @@ class UserSessionFlowCoordinatorStateMachine {
|
|||||||
case showSessionVerificationScreen
|
case showSessionVerificationScreen
|
||||||
/// Session verification has finished
|
/// Session verification has finished
|
||||||
case dismissedSessionVerificationScreen
|
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>
|
private let stateMachine: StateMachine<State, Event>
|
||||||
@@ -74,6 +82,7 @@ class UserSessionFlowCoordinatorStateMachine {
|
|||||||
configure()
|
configure()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// swiftlint:disable:next cyclomatic_complexity
|
||||||
private func configure() {
|
private func configure() {
|
||||||
stateMachine.addRoutes(event: .start, transitions: [.initial => .roomList(selectedRoomId: nil)])
|
stateMachine.addRoutes(event: .start, transitions: [.initial => .roomList(selectedRoomId: nil)])
|
||||||
|
|
||||||
@@ -99,6 +108,10 @@ class UserSessionFlowCoordinatorStateMachine {
|
|||||||
case (.dismissedSessionVerificationScreen, .sessionVerificationScreen(let selectedRoomId)):
|
case (.dismissedSessionVerificationScreen, .sessionVerificationScreen(let selectedRoomId)):
|
||||||
return .roomList(selectedRoomId: selectedRoomId)
|
return .roomList(selectedRoomId: selectedRoomId)
|
||||||
|
|
||||||
|
case (.showStartChatScreen, .roomList(let selectedRoomId)):
|
||||||
|
return .startChatScreen(selectedRoomId: selectedRoomId)
|
||||||
|
case (.dismissedStartChatScreen, .startChatScreen(let selectedRoomId)):
|
||||||
|
return .roomList(selectedRoomId: selectedRoomId)
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -304,6 +304,11 @@ class MockScreen: Identifiable {
|
|||||||
let coordinator = ReportContentCoordinator(parameters: .init(itemID: "test", roomProxy: MockRoomProxy(displayName: "test")))
|
let coordinator = ReportContentCoordinator(parameters: .init(itemID: "test", roomProxy: MockRoomProxy(displayName: "test")))
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
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 roomDetailsScreenWithRoomAvatar
|
||||||
case roomMemberDetailsScreen
|
case roomMemberDetailsScreen
|
||||||
case reportContent
|
case reportContent
|
||||||
|
case startChat
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UITestsScreenIdentifier: CustomStringConvertible {
|
extension UITestsScreenIdentifier: CustomStringConvertible {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2be45f5492a94a96ff2da692738ec68d258805a297b6dec68b86fb14199a3726
|
||||||
|
size 107619
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:153c46f827c106b49d0aeb78d242ac57027424a14899cd421ee9e77ab632310a
|
||||||
|
size 128472
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:4b23d1b10926ea191581b8dc38480e67e34ec0663ae8e87873356edf45f20838
|
||||||
|
size 107029
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3e976ed0e5a41861efb0cb567b2f6311f80cf44b53c54ff453ee9a9af91f1d6a
|
||||||
|
size 126719
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Add the entry point for the Start a new Chat flow, with button on home Screen and first page
|
||||||
Reference in New Issue
Block a user