Home screen - User options menu (#185)

* Add user options menu properties to home screen

* Implement home screen new callbacks

* Add user menu button on home screen

* Add changelog

* Fix unit tests

* Fix user menu button layout, make menu sectioned

* Remove user display name from home screen classes
This commit is contained in:
ismailgulek
2022-09-14 22:42:48 +03:00
committed by GitHub
parent bc457958db
commit b30712b931
11 changed files with 150 additions and 58 deletions
+17
View File
@@ -234,8 +234,12 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
self.stateMachine.processEvent(.showRoomScreen(roomId: roomIdentifier))
case .presentSettings:
self.stateMachine.processEvent(.showSettingsScreen)
case .presentBugReport:
self.presentBugReportScreen()
case .verifySession:
self.stateMachine.processEvent(.showSessionVerificationScreen)
case .signOut:
self.confirmSignOut()
}
}
@@ -335,6 +339,19 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
navigationRouter.present(alert, animated: true)
}
private func confirmSignOut() {
let alert = UIAlertController(title: ElementL10n.actionSignOut,
message: ElementL10n.actionSignOutConfirmationSimple,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: ElementL10n.actionCancel, style: .cancel))
alert.addAction(UIAlertAction(title: ElementL10n.actionSignOut, style: .destructive) { [weak self] _ in
self?.stateMachine.processEvent(.attemptSignOut)
})
navigationRouter.present(alert, animated: true)
}
private func processScreenshotDetection(image: UIImage?, error: Error?) {
MXLog.debug("Detected screenshot: \(String(describing: image)), error: \(String(describing: error))")
@@ -92,7 +92,7 @@ class AppCoordinatorStateMachine {
machine.addRoutes(event: .succeededRestoringSession, transitions: [.restoringSession => .homeScreen])
machine.addRoutes(event: .failedRestoringSession, transitions: [.restoringSession => .signedOut])
machine.addRoutes(event: .attemptSignOut, transitions: [.settingsScreen => .signingOut])
machine.addRoutes(event: .attemptSignOut, transitions: [.any => .signingOut])
machine.addRoutes(event: .succeededSigningOut, transitions: [.signingOut => .signedOut])
machine.addRoutes(event: .failedSigningOut, transitions: [.signingOut => .settingsScreen])
@@ -0,0 +1,22 @@
//
// 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
extension Image {
/// Empty image view
static let empty = Image(uiImage: .init(ciImage: .empty()))
}
@@ -26,7 +26,9 @@ struct HomeScreenCoordinatorParameters {
enum HomeScreenCoordinatorAction {
case presentRoom(roomIdentifier: String)
case presentSettings
case presentBugReport
case verifySession
case signOut
}
final class HomeScreenCoordinator: Coordinator, Presentable {
@@ -53,8 +55,7 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
init(parameters: HomeScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = HomeScreenViewModel(initialDisplayName: parameters.userSession.userID,
attributedStringBuilder: parameters.attributedStringBuilder)
viewModel = HomeScreenViewModel(attributedStringBuilder: parameters.attributedStringBuilder)
let view = HomeScreen(context: viewModel.context)
hostingController = UIHostingController(rootView: view)
@@ -65,8 +66,8 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
switch action {
case .selectRoom(let roomIdentifier):
self.callback?(.presentRoom(roomIdentifier: roomIdentifier))
case .tapUserAvatar:
self.callback?(.presentSettings)
case .userMenu(let action):
self.processUserMenuAction(action)
case .verifySession:
self.callback?(.verifySession)
}
@@ -100,10 +101,6 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
self.viewModel.updateWithUserAvatar(avatar)
}
}
if case let .success(userDisplayName) = await parameters.userSession.clientProxy.loadUserDisplayName() {
self.viewModel.updateWithUserDisplayName(userDisplayName)
}
}
}
@@ -136,4 +133,26 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
viewModel.updateWithRoomSummaries(roomSummaries)
}
private func processUserMenuAction(_ action: HomeScreenViewUserMenuAction) {
switch action {
case .settings:
callback?(.presentSettings)
case .inviteFriends:
presentInviteFriends()
case .feedback:
callback?(.presentBugReport)
case .signOut:
callback?(.signOut)
}
}
private func presentInviteFriends() {
guard let permalink = try? PermalinkBuilder.permalinkTo(userIdentifier: parameters.userSession.userID).absoluteString else {
return
}
let shareText = ElementL10n.inviteFriendsText(ElementInfoPlist.cfBundleName, permalink)
let vc = UIActivityViewController(activityItems: [shareText], applicationActivities: nil)
hostingController.present(vc, animated: true)
}
}
@@ -19,19 +19,25 @@ import UIKit
enum HomeScreenViewModelAction {
case selectRoom(roomIdentifier: String)
case tapUserAvatar
case userMenu(action: HomeScreenViewUserMenuAction)
case verifySession
}
enum HomeScreenViewUserMenuAction {
case settings
case inviteFriends
case feedback
case signOut
}
enum HomeScreenViewAction {
case loadRoomData(roomIdentifier: String)
case selectRoom(roomIdentifier: String)
case tapUserAvatar
case userMenu(action: HomeScreenViewUserMenuAction)
case verifySession
}
struct HomeScreenViewState: BindableState {
var userDisplayName: String
var userAvatar: UIImage?
var showSessionVerificationBanner = false
@@ -39,11 +39,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
// MARK: - Setup
init(initialDisplayName: String, attributedStringBuilder: AttributedStringBuilderProtocol) {
init(attributedStringBuilder: AttributedStringBuilderProtocol) {
self.attributedStringBuilder = attributedStringBuilder
super.init(initialViewState: HomeScreenViewState(userDisplayName: initialDisplayName,
isLoadingRooms: true))
super.init(initialViewState: HomeScreenViewState(isLoadingRooms: true))
}
// MARK: - Public
@@ -54,8 +53,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
loadRoomDataForIdentifier(roomIdentifier)
case .selectRoom(let roomIdentifier):
callback?(.selectRoom(roomIdentifier: roomIdentifier))
case .tapUserAvatar:
callback?(.tapUserAvatar)
case .userMenu(let action):
callback?(.userMenu(action: action))
case .verifySession:
callback?(.verifySession)
}
@@ -113,11 +112,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
func updateWithUserAvatar(_ avatar: UIImage) {
state.userAvatar = avatar
}
func updateWithUserDisplayName(_ displayName: String) {
state.userDisplayName = displayName
}
func showSessionVerificationBanner() {
state.showSessionVerificationBanner = true
}
@@ -24,7 +24,6 @@ protocol HomeScreenViewModelProtocol {
var context: HomeScreenViewModelType.Context { get }
func updateWithUserAvatar(_ avatar: UIImage)
func updateWithUserDisplayName(_ displayName: String)
func updateWithRoomSummaries(_ roomSummaries: [RoomSummaryProtocol])
func showSessionVerificationBanner()
@@ -69,39 +69,71 @@ struct HomeScreen: View {
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button { context.send(viewAction: .tapUserAvatar) } label: {
HStack {
userAvatarImage
.animation(.elementDefault, value: context.viewState.userAvatar)
.transition(.opacity)
userDisplayNameView
.animation(.elementDefault, value: context.viewState.userDisplayName)
.transition(.opacity)
}
}
userMenuButton
}
}
}
@ViewBuilder
private var userAvatarImage: some View {
if let avatar = context.viewState.userAvatar {
Image(uiImage: avatar)
.resizable()
.scaledToFill()
.frame(width: 32, height: 32, alignment: .center)
.clipShape(Circle())
.accessibilityIdentifier("userAvatarImage")
private var userMenuButton: some View {
Menu {
Section {
Button(action: settings) {
Label(ElementL10n.settingsUserSettings, systemImage: "gearshape")
}
}
Section {
Button(action: inviteFriends) {
Label(ElementL10n.inviteFriends, systemImage: "square.and.arrow.up")
}
Button(action: feedback) {
Label(ElementL10n.feedback, systemImage: "questionmark.circle")
}
}
Section {
Button(role: .destructive, action: signOut) {
Label(ElementL10n.actionSignOut, systemImage: "rectangle.portrait.and.arrow.right")
}
}
} label: {
userAvatarImageView
.animation(.elementDefault, value: context.viewState.userAvatar)
.transition(.opacity)
}
}
private var userDisplayNameView: some View {
Text(context.viewState.userDisplayName)
.font(.headline)
.fontWeight(.bold)
.foregroundColor(.primary)
.accessibilityIdentifier("userDisplayNameView")
@ViewBuilder
private var userAvatarImageView: some View {
userAvatarImage
.resizable()
.scaledToFill()
.frame(width: 32, height: 32, alignment: .center)
.clipShape(Circle())
.accessibilityIdentifier("userAvatarImage")
}
private var userAvatarImage: Image {
if let avatar = context.viewState.userAvatar {
return Image(uiImage: avatar)
} else {
return .empty
}
}
private func settings() {
context.send(viewAction: .userMenu(action: .settings))
}
private func inviteFriends() {
context.send(viewAction: .userMenu(action: .inviteFriends))
}
private func feedback() {
context.send(viewAction: .userMenu(action: .feedback))
}
private func signOut() {
context.send(viewAction: .userMenu(action: .signOut))
}
}
@@ -172,8 +204,7 @@ struct HomeScreen_Previews: PreviewProvider {
}
static var body: some View {
let viewModel = HomeScreenViewModel(initialDisplayName: "@username:server.com",
attributedStringBuilder: AttributedStringBuilder())
let viewModel = HomeScreenViewModel(attributedStringBuilder: AttributedStringBuilder())
let eventBrief = EventBrief(eventId: "id",
senderId: "senderId",
@@ -187,7 +218,6 @@ struct HomeScreen_Previews: PreviewProvider {
MockRoomSummary(displayName: "Omega", lastMessage: eventBrief)]
viewModel.updateWithRoomSummaries(roomSummaries)
viewModel.updateWithUserDisplayName("username")
if let avatarImage = UIImage(systemName: "person.fill") {
viewModel.updateWithUserAvatar(avatarImage)