[Spaces] Space menu #4494

- Implemented Leave feature
- UI & code tweaks
This commit is contained in:
Gil Eluard 2021-07-23 11:00:16 +02:00
parent 9402a5b4f5
commit fbabdfa6f2
10 changed files with 193 additions and 27 deletions

View file

@ -1657,6 +1657,7 @@ Tap the + to start adding people.";
"spaces_home_space_title" = "Home";
"spaces_left_panel_title" = "Spaces";
"leave_space_message" = "Are you sure you want to leave %@?";
// Mark: Avatar

View file

@ -2062,6 +2062,10 @@ internal enum VectorL10n {
internal static var leave: String {
return VectorL10n.tr("Vector", "leave")
}
/// Are you sure you want to leave %@?
internal static func leaveSpaceMessage(_ p1: String) -> String {
return VectorL10n.tr("Vector", "leave_space_message", p1)
}
/// Less
internal static var less: String {
return VectorL10n.tr("Vector", "less")

View file

@ -18,11 +18,11 @@
import Foundation
protocol SideMenuViewModelViewDelegate: class {
protocol SideMenuViewModelViewDelegate: AnyObject {
func sideMenuViewModel(_ viewModel: SideMenuViewModelType, didUpdateViewState viewSate: SideMenuViewState)
}
protocol SideMenuViewModelCoordinatorDelegate: class {
protocol SideMenuViewModelCoordinatorDelegate: AnyObject {
func sideMenuViewModel(_ viewModel: SideMenuViewModelType, didTapMenuItem menuItem: SideMenuItem, fromSourceView sourceView: UIView)
}

View file

@ -42,7 +42,7 @@ class SpaceMenuListViewCell: UITableViewCell, Themable, NibReusable {
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
UIView.animate(withDuration: !animated ? 0.3 : 0.0) {
UIView.animate(withDuration: animated ? 0.3 : 0.0) {
self.selectionView.alpha = selected ? 1.0 : 0.0
}
}

View file

@ -26,11 +26,12 @@ class SpaceMenuPresenter: NSObject {
// MARK: Private
private weak var presentingViewController: UIViewController?
private let viewModel = SpaceMenuViewModel()
private var viewModel: SpaceMenuViewModel!
private weak var sourceView: UIView?
private lazy var slidingModalPresenter: SlidingModalPresenter = {
return SlidingModalPresenter()
}()
private weak var selectedSpace: MXSpace?
// MARK: - Public
@ -39,10 +40,11 @@ class SpaceMenuPresenter: NSObject {
sourceView: UIView?,
session: MXSession,
animated: Bool) {
self.viewModel = SpaceMenuViewModel(session: session, spaceId: spaceId)
self.viewModel.coordinatorDelegate = self
self.presentingViewController = viewController
self.sourceView = sourceView
self.selectedSpace = session.spaceService.getSpace(withId: spaceId)
self.showMenu(for: spaceId, session: session)
}
@ -60,34 +62,43 @@ class SpaceMenuPresenter: NSObject {
private func present(_ viewController: SpaceMenuViewController, animated: Bool) {
// if UIDevice.current.isPhone {
if UIDevice.current.isPhone {
guard let rootViewController = self.presentingViewController else {
MXLog.error("[SpaceMenuPresenter] present no rootViewController found")
return
}
slidingModalPresenter.present(viewController, from: rootViewController.presentedViewController ?? rootViewController, animated: true, completion: nil)
// } else {
} else {
// Configure source view when view controller is presented with a popover
// viewController.modalPresentationStyle = .popover
// if let sourceView = self.sourceView, let popoverPresentationController = viewController.popoverPresentationController {
// popoverPresentationController.sourceView = sourceView
// popoverPresentationController.sourceRect = sourceView.bounds
// }
//
// self.presentingViewController?.present(viewController, animated: animated, completion: nil)
// }
viewController.modalPresentationStyle = .popover
if let sourceView = self.sourceView, let popoverPresentationController = viewController.popoverPresentationController {
popoverPresentationController.sourceView = sourceView
popoverPresentationController.sourceRect = sourceView.bounds
}
self.presentingViewController?.present(viewController, animated: animated, completion: nil)
}
}
}
// MARK: - SpaceMenuModelViewModelCoordinatorDelegate
extension SpaceMenuPresenter: SpaceMenuModelViewModelCoordinatorDelegate {
func spaceListViewModelDidDismiss(_ viewModel: SpaceMenuViewModelType) {
func spaceMenuViewModelDidDismiss(_ viewModel: SpaceMenuViewModelType) {
self.dismiss(animated: true, completion: nil)
}
func spaceListViewModel(_ viewModel: SpaceMenuViewModelType, didSelectItemWithId itemId: String) {
self.dismiss(animated: true, completion: nil)
func spaceMenuViewModel(_ viewModel: SpaceMenuViewModelType, didSelectItemWithId itemId: String) {
let actionId = SpaceMenuViewModel.ActionId(rawValue: itemId)
switch actionId {
case .leave: break
case .members:
self.dismiss(animated: true, completion: nil)
case .rooms:
self.dismiss(animated: true, completion: nil)
default:
MXLog.error("[SpaceMenuPresenter] spaceListViewModel didSelectItemWithId: invalid itemId \(itemId)")
}
}
}

View file

@ -98,6 +98,7 @@
<modalPageSheetSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="avatarView" destination="aSn-OV-epF" id="kgk-RU-l5L"/>
<outlet property="bottomMargin" destination="8cn-Zi-aY3" id="jCd-eZ-Jz0"/>
<outlet property="closeButton" destination="dxd-y5-bn4" id="T5W-Ah-JMq"/>
<outlet property="subtitleLabel" destination="Flc-Ew-aDd" id="vaA-iC-rfS"/>
<outlet property="tableView" destination="TI7-FD-nIm" id="WSM-hN-CQQ"/>

View file

@ -31,7 +31,9 @@ class SpaceMenuViewController: UIViewController {
private var session: MXSession!
private var spaceId: String!
private var viewModel: SpaceMenuViewModelType!
private var errorPresenter: MXKErrorPresentation!
private var activityPresenter: ActivityIndicatorPresenter!
// MARK: Outlets
@IBOutlet private weak var avatarView: SpaceAvatarView!
@ -39,6 +41,7 @@ class SpaceMenuViewController: UIViewController {
@IBOutlet private weak var subtitleLabel: UILabel!
@IBOutlet private weak var closeButton: UIButton!
@IBOutlet private weak var tableView: UITableView!
@IBOutlet private weak var bottomMargin: NSLayoutConstraint!
// MARK: - Setup
@ -59,9 +62,13 @@ class SpaceMenuViewController: UIViewController {
// Do any additional setup after loading the view.
self.setupViews()
self.activityPresenter = ActivityIndicatorPresenter()
self.errorPresenter = MXKErrorAlertPresentation()
self.registerThemeServiceDidChangeThemeNotification()
self.update(theme: self.theme)
self.viewModel.viewDelegate = self
}
override var preferredStatusBarStyle: UIStatusBarStyle {
@ -70,7 +77,7 @@ class SpaceMenuViewController: UIViewController {
override var preferredContentSize: CGSize {
get {
return CGSize(width: 300, height: 300)
return CGSize(width: 320, height: self.tableView.frame.minY + Constants.estimatedRowHeight * CGFloat(self.viewModel.menuItems.count) + self.bottomMargin.constant)
}
set {
super.preferredContentSize = newValue
@ -134,6 +141,45 @@ class SpaceMenuViewController: UIViewController {
self.tableView.register(cellType: SpaceMenuListViewCell.self)
self.tableView.tableFooterView = UIView()
}
private func render(viewState: SpaceMenuViewState) {
switch viewState {
case .loading:
self.renderLoading()
case .loaded:
self.renderLoaded()
case .alert(let alert):
self.render(alert: alert)
case .error(let error):
self.render(error: error)
case .deselect:
self.renderDeselect()
}
}
private func renderLoading() {
self.activityPresenter.presentActivityIndicator(on: self.view, animated: true)
}
private func renderLoaded() {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.renderDeselect()
}
private func render(error: Error) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil)
}
private func render(alert: UIAlertController) {
self.present(alert, animated: true, completion: nil)
}
private func renderDeselect() {
if let selectedRow = self.tableView.indexPathForSelectedRow {
self.tableView.deselectRow(at: selectedRow, animated: true)
}
}
}
// MARK: - SlidingModalPresentable
@ -145,11 +191,19 @@ extension SpaceMenuViewController: SlidingModalPresentable {
}
func layoutHeightFittingWidth(_ width: CGFloat) -> CGFloat {
return 300
return self.preferredContentSize.height
}
}
// MARK: - SpaceMenuViewModelViewDelegate
extension SpaceMenuViewController: SpaceMenuViewModelViewDelegate {
func spaceMenuViewModel(_ viewModel: SpaceMenuViewModelType, didUpdateViewState viewSate: SpaceMenuViewState) {
self.render(viewState: viewSate)
}
}
// MARK: - UITableViewDataSource
extension SpaceMenuViewController: UITableViewDataSource {
@ -175,6 +229,7 @@ extension SpaceMenuViewController: UITableViewDataSource {
}
// MARK: - UITableViewDelegate
extension SpaceMenuViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

View file

@ -30,21 +30,83 @@ class SpaceMenuViewModel: SpaceMenuViewModelType {
// MARK: - Properties
weak var coordinatorDelegate: SpaceMenuModelViewModelCoordinatorDelegate?
weak var viewDelegate: SpaceMenuViewModelViewDelegate?
var menuItems: [SpaceMenuListItemViewData] = [
SpaceMenuListItemViewData(actionId: ActionId.members.rawValue, style: .normal, title: VectorL10n.roomDetailsPeople, icon: UIImage(named: "space_menu_members")),
SpaceMenuListItemViewData(actionId: ActionId.rooms.rawValue, style: .normal, title: VectorL10n.groupDetailsRooms, icon: UIImage(named: "space_menu_rooms")),
SpaceMenuListItemViewData(actionId: ActionId.leave.rawValue, style: .destructive, title: VectorL10n.leave, icon: UIImage(named: "space_menu_leave"))
]
private let session: MXSession
private let spaceId: String
// MARK: - Setup
init(session: MXSession, spaceId: String) {
self.session = session
self.spaceId = spaceId
}
// MARK: - Public
func process(viewAction: SpaceMenuViewAction) {
switch viewAction {
case .dismiss:
self.coordinatorDelegate?.spaceListViewModelDidDismiss(self)
self.coordinatorDelegate?.spaceMenuViewModelDidDismiss(self)
case .selectRow(at: let indexPath):
self.coordinatorDelegate?.spaceListViewModel(self, didSelectItemWithId: menuItems[indexPath.row].actionId)
self.processAction(with: menuItems[indexPath.row].actionId)
}
}
// MARK: - Private
private func processAction(with actionStringId: String) {
let actionId = ActionId(rawValue: actionStringId)
switch actionId {
case .leave:
self.leaveSpace()
default:
self.coordinatorDelegate?.spaceMenuViewModel(self, didSelectItemWithId: actionStringId)
}
}
private func leaveSpace() {
guard let space = self.session.spaceService.getSpace(withId: self.spaceId), let displayName = space.summary?.displayname else {
return
}
let alert = UIAlertController(title: nil, message: VectorL10n.leaveSpaceMessage(displayName), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: VectorL10n.leave, style: .destructive, handler: { [weak self] action in
guard let self = self else {
return
}
self.viewDelegate?.spaceMenuViewModel(self, didUpdateViewState: .loading)
space.room.leave(completion: { [weak self] response in
guard let self = self else {
return
}
self.viewDelegate?.spaceMenuViewModel(self, didUpdateViewState: .loaded)
if let error = response.error {
self.viewDelegate?.spaceMenuViewModel(self, didUpdateViewState: .error(error))
} else {
self.process(viewAction: .dismiss)
}
})
}))
alert.addAction(UIAlertAction(title: VectorL10n.cancel, style: .cancel, handler: { [weak self] action in
guard let self = self else {
return
}
self.viewDelegate?.spaceMenuViewModel(self, didUpdateViewState: .deselect)
}))
self.viewDelegate?.spaceMenuViewModel(self, didUpdateViewState: .alert(alert))
}
}

View file

@ -16,14 +16,20 @@
import Foundation
protocol SpaceMenuViewModelViewDelegate: AnyObject {
func spaceMenuViewModel(_ viewModel: SpaceMenuViewModelType, didUpdateViewState viewSate: SpaceMenuViewState)
}
protocol SpaceMenuModelViewModelCoordinatorDelegate: AnyObject {
func spaceListViewModelDidDismiss(_ viewModel: SpaceMenuViewModelType)
func spaceListViewModel(_ viewModel: SpaceMenuViewModelType, didSelectItemWithId itemId: String)
func spaceMenuViewModelDidDismiss(_ viewModel: SpaceMenuViewModelType)
func spaceMenuViewModel(_ viewModel: SpaceMenuViewModelType, didSelectItemWithId itemId: String)
}
/// Protocol describing the view model used by `SpaceMenuViewController`
protocol SpaceMenuViewModelType {
var menuItems: [SpaceMenuListItemViewData] { get }
var viewDelegate: SpaceMenuViewModelViewDelegate? { get set }
var coordinatorDelegate: SpaceMenuModelViewModelCoordinatorDelegate? { get set }
func process(viewAction: SpaceMenuViewAction)

View file

@ -0,0 +1,26 @@
//
// Copyright 2021 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
/// SpaceMenuViewController view state
enum SpaceMenuViewState {
case loading
case loaded
case deselect
case alert(UIAlertController)
case error(Error)
}