Skip to content

Commit

Permalink
[Spaces] Space menu #4494
Browse files Browse the repository at this point in the history
- Implemented Leave feature
- UI & code tweaks
  • Loading branch information
gileluard committed Jul 23, 2021
1 parent 9402a5b commit fbabdfa
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 27 deletions.
1 change: 1 addition & 0 deletions Riot/Assets/en.lproj/Vector.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions Riot/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 2 additions & 2 deletions Riot/Modules/SideMenu/SideMenuViewModelType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
2 changes: 1 addition & 1 deletion Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
41 changes: 26 additions & 15 deletions Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
}
Expand All @@ -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)")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"/>
Expand Down
63 changes: 59 additions & 4 deletions Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,17 @@ 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!
@IBOutlet private weak var titleLabel: UILabel!
@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

Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -175,6 +229,7 @@ extension SpaceMenuViewController: UITableViewDataSource {
}

// MARK: - UITableViewDelegate

extension SpaceMenuViewController: UITableViewDelegate {

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
Expand Down
68 changes: 65 additions & 3 deletions Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
10 changes: 8 additions & 2 deletions Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModelType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
26 changes: 26 additions & 0 deletions Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewState.swift
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit fbabdfa

Please sign in to comment.