mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +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:
@@ -24,8 +24,10 @@ struct DeveloperOptionsScreenViewState: BindableState {
|
||||
|
||||
struct DeveloperOptionsScreenViewStateBindings {
|
||||
var shouldCollapseRoomStateEvents: Bool
|
||||
var showStartChatFlow: Bool
|
||||
}
|
||||
|
||||
enum DeveloperOptionsScreenViewAction {
|
||||
case changedShouldCollapseRoomStateEvents
|
||||
case changedShowStartChatFlow
|
||||
}
|
||||
|
||||
+6
-1
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user