mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 15:22:39 +00:00
[Spaces] Space menu #4494
- Implemented Leave feature - UI & code tweaks
This commit is contained in:
parent
9402a5b4f5
commit
fbabdfa6f2
10 changed files with 193 additions and 27 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
26
Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewState.swift
Normal file
26
Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewState.swift
Normal 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)
|
||||
}
|
Loading…
Reference in a new issue