mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-05-05 23:47:44 +02:00
Merge branch 'gil/SP1_space_creation' of github.com:vector-im/element-ios into gil/SP1_space_creation
This commit is contained in:
@@ -1889,7 +1889,11 @@ Tap the + to start adding people.";
|
|||||||
"spaces_add_space" = "Add space";
|
"spaces_add_space" = "Add space";
|
||||||
"space_public_join_rule_detail" = "Open to anyone, best for communities";
|
"space_public_join_rule_detail" = "Open to anyone, best for communities";
|
||||||
|
|
||||||
"space_topic" = "description";
|
"space_topic" = "Description";
|
||||||
|
|
||||||
|
"space_settings_access_section" = "Who can access this space?";
|
||||||
|
"space_settings_update_failed_message" = "Failed to update space settings. Do you want to retry?";
|
||||||
|
"space_settings_current_address_message" = "Your space is viewable at\n%@";
|
||||||
|
|
||||||
// Mark: - Space Creation
|
// Mark: - Space Creation
|
||||||
|
|
||||||
|
|||||||
@@ -5555,11 +5555,23 @@ public class VectorL10n: NSObject {
|
|||||||
public static var spacePublicJoinRuleDetail: String {
|
public static var spacePublicJoinRuleDetail: String {
|
||||||
return VectorL10n.tr("Vector", "space_public_join_rule_detail")
|
return VectorL10n.tr("Vector", "space_public_join_rule_detail")
|
||||||
}
|
}
|
||||||
|
/// Who can access this space?
|
||||||
|
public static var spaceSettingsAccessSection: String {
|
||||||
|
return VectorL10n.tr("Vector", "space_settings_access_section")
|
||||||
|
}
|
||||||
|
/// Your space is viewable at\n%@
|
||||||
|
public static func spaceSettingsCurrentAddressMessage(_ p1: String) -> String {
|
||||||
|
return VectorL10n.tr("Vector", "space_settings_current_address_message", p1)
|
||||||
|
}
|
||||||
|
/// Failed to update space settings. Do you want to retry?
|
||||||
|
public static var spaceSettingsUpdateFailedMessage: String {
|
||||||
|
return VectorL10n.tr("Vector", "space_settings_update_failed_message")
|
||||||
|
}
|
||||||
/// space
|
/// space
|
||||||
public static var spaceTag: String {
|
public static var spaceTag: String {
|
||||||
return VectorL10n.tr("Vector", "space_tag")
|
return VectorL10n.tr("Vector", "space_tag")
|
||||||
}
|
}
|
||||||
/// description
|
/// Description
|
||||||
public static var spaceTopic: String {
|
public static var spaceTopic: String {
|
||||||
return VectorL10n.tr("Vector", "space_topic")
|
return VectorL10n.tr("Vector", "space_topic")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
|
|||||||
private let room: MXRoom
|
private let room: MXRoom
|
||||||
private let parentSpaceId: String?
|
private let parentSpaceId: String?
|
||||||
private let initialSection: RoomInfoSection
|
private let initialSection: RoomInfoSection
|
||||||
|
private let dismissOnCancel: Bool
|
||||||
private weak var roomSettingsViewController: RoomSettingsViewController?
|
private weak var roomSettingsViewController: RoomSettingsViewController?
|
||||||
|
|
||||||
private lazy var segmentedViewController: SegmentedViewController = {
|
private lazy var segmentedViewController: SegmentedViewController = {
|
||||||
@@ -103,6 +104,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
|
|||||||
self.room = parameters.room
|
self.room = parameters.room
|
||||||
self.parentSpaceId = parameters.parentSpaceId
|
self.parentSpaceId = parameters.parentSpaceId
|
||||||
self.initialSection = parameters.initialSection
|
self.initialSection = parameters.initialSection
|
||||||
|
self.dismissOnCancel = parameters.dismissOnCancel
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public methods
|
// MARK: - Public methods
|
||||||
@@ -226,6 +228,22 @@ extension RoomInfoCoordinator: RoomNotificationSettingsCoordinatorDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension RoomInfoCoordinator: RoomSettingsViewControllerDelegate {
|
extension RoomInfoCoordinator: RoomSettingsViewControllerDelegate {
|
||||||
|
func roomSettingsViewControllerDidCancel(_ controller: RoomSettingsViewController!) {
|
||||||
|
if self.dismissOnCancel {
|
||||||
|
self.navigationRouter.dismissModule(animated: true, completion: nil)
|
||||||
|
} else {
|
||||||
|
controller.withdrawViewController(animated: true) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func roomSettingsViewControllerDidComplete(_ controller: RoomSettingsViewController!) {
|
||||||
|
if self.dismissOnCancel {
|
||||||
|
self.navigationRouter.dismissModule(animated: true, completion: nil)
|
||||||
|
} else {
|
||||||
|
controller.withdrawViewController(animated: true) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func roomSettingsViewController(_ controller: RoomSettingsViewController!, didReplaceRoomWithReplacementId newRoomId: String!) {
|
func roomSettingsViewController(_ controller: RoomSettingsViewController!, didReplaceRoomWithReplacementId newRoomId: String!) {
|
||||||
self.delegate?.roomInfoCoordinator(self, didReplaceRoomWithReplacementId: newRoomId)
|
self.delegate?.roomInfoCoordinator(self, didReplaceRoomWithReplacementId: newRoomId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,16 +32,22 @@ class RoomInfoCoordinatorParameters: NSObject {
|
|||||||
let room: MXRoom
|
let room: MXRoom
|
||||||
let parentSpaceId: String?
|
let parentSpaceId: String?
|
||||||
let initialSection: RoomInfoSection
|
let initialSection: RoomInfoSection
|
||||||
|
let dismissOnCancel: Bool
|
||||||
|
|
||||||
init(session: MXSession, room: MXRoom, parentSpaceId: String?, initialSection: RoomInfoSection) {
|
init(session: MXSession, room: MXRoom, parentSpaceId: String?, initialSection: RoomInfoSection, dismissOnCancel: Bool) {
|
||||||
self.session = session
|
self.session = session
|
||||||
self.room = room
|
self.room = room
|
||||||
self.parentSpaceId = parentSpaceId
|
self.parentSpaceId = parentSpaceId
|
||||||
self.initialSection = initialSection
|
self.initialSection = initialSection
|
||||||
|
self.dismissOnCancel = dismissOnCancel
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
convenience init(session: MXSession, room: MXRoom, parentSpaceId: String?) {
|
convenience init(session: MXSession, room: MXRoom, parentSpaceId: String?) {
|
||||||
self.init(session: session, room: room, parentSpaceId: parentSpaceId, initialSection: .none)
|
self.init(session: session, room: room, parentSpaceId: parentSpaceId, initialSection: .none, dismissOnCancel: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
convenience init(session: MXSession, room: MXRoom, parentSpaceId: String?, initialSection: RoomInfoSection) {
|
||||||
|
self.init(session: session, room: room, parentSpaceId: parentSpaceId, initialSection: initialSection, dismissOnCancel: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,4 +71,8 @@ typedef enum : NSUInteger {
|
|||||||
|
|
||||||
- (void)roomSettingsViewController:(RoomSettingsViewController *)controller didReplaceRoomWithReplacementId:(NSString *)newRoomId;
|
- (void)roomSettingsViewController:(RoomSettingsViewController *)controller didReplaceRoomWithReplacementId:(NSString *)newRoomId;
|
||||||
|
|
||||||
|
- (void)roomSettingsViewControllerDidCancel:(RoomSettingsViewController *)controller;
|
||||||
|
|
||||||
|
- (void)roomSettingsViewControllerDidComplete:(RoomSettingsViewController *)controller;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -906,7 +906,14 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
|
|||||||
|
|
||||||
[self->updatedItemsDict removeAllObjects];
|
[self->updatedItemsDict removeAllObjects];
|
||||||
|
|
||||||
[self withdrawViewControllerAnimated:YES completion:nil];
|
if (self.delegate)
|
||||||
|
{
|
||||||
|
[self.delegate roomSettingsViewControllerDidCancel:self];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
[self withdrawViewControllerAnimated:YES completion:nil];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}]];
|
}]];
|
||||||
@@ -1435,7 +1442,14 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
[self withdrawViewControllerAnimated:YES completion:nil];
|
if (self.delegate)
|
||||||
|
{
|
||||||
|
[self.delegate roomSettingsViewControllerDidCancel:self];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
[self withdrawViewControllerAnimated:YES completion:nil];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2183,7 +2197,14 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
|
|||||||
|
|
||||||
[self stopActivityIndicator];
|
[self stopActivityIndicator];
|
||||||
|
|
||||||
[self withdrawViewControllerAnimated:YES completion:nil];
|
if (self.delegate)
|
||||||
|
{
|
||||||
|
[self.delegate roomSettingsViewControllerDidComplete:self];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
[self withdrawViewControllerAnimated:YES completion:nil];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - UITableViewDataSource
|
#pragma mark - UITableViewDataSource
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType {
|
|||||||
private var membersCoordinator: SpaceMembersCoordinator?
|
private var membersCoordinator: SpaceMembersCoordinator?
|
||||||
private var createSpaceCoordinator: SpaceCreationCoordinator?
|
private var createSpaceCoordinator: SpaceCreationCoordinator?
|
||||||
private var createRoomCoordinator: CreateRoomCoordinator?
|
private var createRoomCoordinator: CreateRoomCoordinator?
|
||||||
|
private var spaceSettingsCoordinator: Coordinator?
|
||||||
|
|
||||||
// MARK: Public
|
// MARK: Public
|
||||||
|
|
||||||
@@ -297,10 +298,27 @@ final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType {
|
|||||||
presentable.presentationController?.delegate = self
|
presentable.presentationController?.delegate = self
|
||||||
toPresentable().present(presentable, animated: true, completion: nil)
|
toPresentable().present(presentable, animated: true, completion: nil)
|
||||||
createRoomCoordinator.start()
|
createRoomCoordinator.start()
|
||||||
self.add(childCoordinator: createRoomCoordinator)
|
|
||||||
self.createRoomCoordinator = createRoomCoordinator
|
self.createRoomCoordinator = createRoomCoordinator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
private func showSpaceSettings(spaceId: String, session: MXSession) {
|
||||||
|
let coordinator = SpaceSettingsModalCoordinator(parameters: SpaceSettingsModalCoordinatorParameters(session: session, spaceId: spaceId))
|
||||||
|
coordinator.callback = { [weak self] result in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
coordinator.toPresentable().dismiss(animated: true) {
|
||||||
|
self.spaceSettingsCoordinator = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let presentable = coordinator.toPresentable()
|
||||||
|
presentable.presentationController?.delegate = self
|
||||||
|
toPresentable().present(presentable, animated: true, completion: nil)
|
||||||
|
coordinator.start()
|
||||||
|
self.spaceSettingsCoordinator = coordinator
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: UserSessions management
|
// MARK: UserSessions management
|
||||||
|
|
||||||
private func registerUserSessionsServiceNotifications() {
|
private func registerUserSessionsServiceNotifications() {
|
||||||
@@ -405,7 +423,11 @@ extension SideMenuCoordinator: SpaceMenuPresenterDelegate {
|
|||||||
case .addSpace:
|
case .addSpace:
|
||||||
AppDelegate.theDelegate().showAlert(withTitle: VectorL10n.spacesAddSpace, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName))
|
AppDelegate.theDelegate().showAlert(withTitle: VectorL10n.spacesAddSpace, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName))
|
||||||
case .settings:
|
case .settings:
|
||||||
AppDelegate.theDelegate().showAlert(withTitle: VectorL10n.sideMenuActionSettings, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName))
|
if #available(iOS 14.0, *) {
|
||||||
|
self.showSpaceSettings(spaceId: spaceId, session: session)
|
||||||
|
} else {
|
||||||
|
AppDelegate.theDelegate().showAlert(withTitle: VectorL10n.settingsTitle, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -448,7 +470,6 @@ extension SideMenuCoordinator: SpaceMembersCoordinatorDelegate {
|
|||||||
extension SideMenuCoordinator: CreateRoomCoordinatorDelegate {
|
extension SideMenuCoordinator: CreateRoomCoordinatorDelegate {
|
||||||
func createRoomCoordinator(_ coordinator: CreateRoomCoordinatorType, didCreateNewRoom room: MXRoom) {
|
func createRoomCoordinator(_ coordinator: CreateRoomCoordinatorType, didCreateNewRoom room: MXRoom) {
|
||||||
coordinator.toPresentable().dismiss(animated: true) {
|
coordinator.toPresentable().dismiss(animated: true) {
|
||||||
self.remove(childCoordinator: coordinator)
|
|
||||||
self.createRoomCoordinator = nil
|
self.createRoomCoordinator = nil
|
||||||
self.parameters.appNavigator.sideMenu.dismiss(animated: true) {
|
self.parameters.appNavigator.sideMenu.dismiss(animated: true) {
|
||||||
|
|
||||||
@@ -461,7 +482,6 @@ extension SideMenuCoordinator: CreateRoomCoordinatorDelegate {
|
|||||||
|
|
||||||
func createRoomCoordinator(_ coordinator: CreateRoomCoordinatorType, didAddRoomsWithIds roomIds: [String]) {
|
func createRoomCoordinator(_ coordinator: CreateRoomCoordinatorType, didAddRoomsWithIds roomIds: [String]) {
|
||||||
coordinator.toPresentable().dismiss(animated: true) {
|
coordinator.toPresentable().dismiss(animated: true) {
|
||||||
self.remove(childCoordinator: coordinator)
|
|
||||||
self.createRoomCoordinator = nil
|
self.createRoomCoordinator = nil
|
||||||
self.parameters.appNavigator.sideMenu.dismiss(animated: true) {
|
self.parameters.appNavigator.sideMenu.dismiss(animated: true) {
|
||||||
|
|
||||||
@@ -474,7 +494,6 @@ extension SideMenuCoordinator: CreateRoomCoordinatorDelegate {
|
|||||||
|
|
||||||
func createRoomCoordinatorDidCancel(_ coordinator: CreateRoomCoordinatorType) {
|
func createRoomCoordinatorDidCancel(_ coordinator: CreateRoomCoordinatorType) {
|
||||||
coordinator.toPresentable().dismiss(animated: true) {
|
coordinator.toPresentable().dismiss(animated: true) {
|
||||||
self.remove(childCoordinator: coordinator)
|
|
||||||
self.createRoomCoordinator = nil
|
self.createRoomCoordinator = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -488,5 +507,6 @@ extension SideMenuCoordinator: UIAdaptivePresentationControllerDelegate {
|
|||||||
self.membersCoordinator = nil
|
self.membersCoordinator = nil
|
||||||
self.createSpaceCoordinator = nil
|
self.createSpaceCoordinator = nil
|
||||||
self.createRoomCoordinator = nil
|
self.createRoomCoordinator = nil
|
||||||
|
self.spaceSettingsCoordinator = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ struct SpaceMemberDetailCoordinatorParameters {
|
|||||||
let member: MXRoomMember
|
let member: MXRoomMember
|
||||||
let session: MXSession
|
let session: MXSession
|
||||||
let spaceId: String
|
let spaceId: String
|
||||||
|
let showCancelMenuItem: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
final class SpaceMemberDetailCoordinator: NSObject, SpaceMemberDetailCoordinatorType {
|
final class SpaceMemberDetailCoordinator: NSObject, SpaceMemberDetailCoordinatorType {
|
||||||
@@ -49,7 +50,7 @@ final class SpaceMemberDetailCoordinator: NSObject, SpaceMemberDetailCoordinator
|
|||||||
init(parameters: SpaceMemberDetailCoordinatorParameters) {
|
init(parameters: SpaceMemberDetailCoordinatorParameters) {
|
||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
|
|
||||||
let spaceMemberDetailViewModel = SpaceMemberDetailViewModel(userSessionsService: parameters.userSessionsService, session: parameters.session, member: parameters.member, spaceId: parameters.spaceId)
|
let spaceMemberDetailViewModel = SpaceMemberDetailViewModel(userSessionsService: parameters.userSessionsService, session: parameters.session, member: parameters.member, spaceId: parameters.spaceId, showCancelMenuItem: parameters.showCancelMenuItem)
|
||||||
let spaceMemberDetailViewController = SpaceMemberDetailViewController.instantiate(with: spaceMemberDetailViewModel)
|
let spaceMemberDetailViewController = SpaceMemberDetailViewController.instantiate(with: spaceMemberDetailViewModel)
|
||||||
spaceMemberDetailViewController.enableMention = true
|
spaceMemberDetailViewController.enableMention = true
|
||||||
spaceMemberDetailViewController.enableVoipCall = false
|
spaceMemberDetailViewController.enableVoipCall = false
|
||||||
|
|||||||
@@ -106,11 +106,13 @@ final class SpaceMemberDetailViewController: RoomMemberDetailsViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func setupViews() {
|
private func setupViews() {
|
||||||
let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in
|
if viewModel.showCancelMenuItem {
|
||||||
self?.cancelButtonAction()
|
let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in
|
||||||
|
self?.cancelButtonAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.navigationItem.rightBarButtonItem = cancelBarButtonItem
|
||||||
}
|
}
|
||||||
|
|
||||||
self.navigationItem.rightBarButtonItem = cancelBarButtonItem
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func render(viewState: SpaceMemberDetailViewState) {
|
private func render(viewState: SpaceMemberDetailViewState) {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ final class SpaceMemberDetailViewModel: NSObject, SpaceMemberDetailViewModelType
|
|||||||
private let member: MXRoomMember
|
private let member: MXRoomMember
|
||||||
private let spaceId: String
|
private let spaceId: String
|
||||||
private var space: MXSpace?
|
private var space: MXSpace?
|
||||||
|
private(set) var showCancelMenuItem: Bool
|
||||||
|
|
||||||
private var currentOperation: MXHTTPOperation?
|
private var currentOperation: MXHTTPOperation?
|
||||||
|
|
||||||
@@ -39,11 +40,12 @@ final class SpaceMemberDetailViewModel: NSObject, SpaceMemberDetailViewModelType
|
|||||||
|
|
||||||
// MARK: - Setup
|
// MARK: - Setup
|
||||||
|
|
||||||
init(userSessionsService: UserSessionsService, session: MXSession, member: MXRoomMember, spaceId: String) {
|
init(userSessionsService: UserSessionsService, session: MXSession, member: MXRoomMember, spaceId: String, showCancelMenuItem: Bool) {
|
||||||
self.userSessionsService = userSessionsService
|
self.userSessionsService = userSessionsService
|
||||||
self.session = session
|
self.session = session
|
||||||
self.member = member
|
self.member = member
|
||||||
self.spaceId = spaceId
|
self.spaceId = spaceId
|
||||||
|
self.showCancelMenuItem = showCancelMenuItem
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ protocol SpaceMemberDetailViewModelType {
|
|||||||
|
|
||||||
var viewDelegate: SpaceMemberDetailViewModelViewDelegate? { get set }
|
var viewDelegate: SpaceMemberDetailViewModelViewDelegate? { get set }
|
||||||
var coordinatorDelegate: SpaceMemberDetailViewModelCoordinatorDelegate? { get set }
|
var coordinatorDelegate: SpaceMemberDetailViewModelCoordinatorDelegate? { get set }
|
||||||
|
var showCancelMenuItem: Bool { get }
|
||||||
|
|
||||||
func process(viewAction: SpaceMemberDetailViewAction)
|
func process(viewAction: SpaceMemberDetailViewAction)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ final class SpaceMemberListViewController: RoomParticipantsViewController {
|
|||||||
self?.cancelButtonAction()
|
self?.cancelButtonAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
self.navigationItem.rightBarButtonItem = cancelBarButtonItem
|
self.navigationItem.leftBarButtonItem = cancelBarButtonItem
|
||||||
|
|
||||||
self.titleView = MainTitleView()
|
self.titleView = MainTitleView()
|
||||||
self.titleView.titleLabel.text = VectorL10n.roomDetailsPeople
|
self.titleView.titleLabel.text = VectorL10n.roomDetailsPeople
|
||||||
|
|||||||
@@ -22,6 +22,17 @@ struct SpaceMembersCoordinatorParameters {
|
|||||||
let userSessionsService: UserSessionsService
|
let userSessionsService: UserSessionsService
|
||||||
let session: MXSession
|
let session: MXSession
|
||||||
let spaceId: String
|
let spaceId: String
|
||||||
|
let navigationRouter: NavigationRouterType
|
||||||
|
|
||||||
|
init(userSessionsService: UserSessionsService,
|
||||||
|
session: MXSession,
|
||||||
|
spaceId: String,
|
||||||
|
navigationRouter: NavigationRouterType = NavigationRouter(navigationController: RiotNavigationController())) {
|
||||||
|
self.userSessionsService = userSessionsService
|
||||||
|
self.session = session
|
||||||
|
self.spaceId = spaceId
|
||||||
|
self.navigationRouter = navigationRouter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objcMembers
|
@objcMembers
|
||||||
@@ -46,7 +57,7 @@ final class SpaceMembersCoordinator: SpaceMembersCoordinatorType {
|
|||||||
|
|
||||||
init(parameters: SpaceMembersCoordinatorParameters) {
|
init(parameters: SpaceMembersCoordinatorParameters) {
|
||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
self.navigationRouter = NavigationRouter(navigationController: RiotNavigationController())
|
self.navigationRouter = parameters.navigationRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public methods
|
// MARK: - Public methods
|
||||||
@@ -59,8 +70,15 @@ final class SpaceMembersCoordinator: SpaceMembersCoordinatorType {
|
|||||||
|
|
||||||
self.add(childCoordinator: rootCoordinator)
|
self.add(childCoordinator: rootCoordinator)
|
||||||
|
|
||||||
self.navigationRouter.setRootModule(rootCoordinator)
|
if self.navigationRouter.modules.isEmpty {
|
||||||
}
|
self.navigationRouter.setRootModule(rootCoordinator)
|
||||||
|
} else {
|
||||||
|
self.navigationRouter.push(rootCoordinator, animated: true) {
|
||||||
|
self.remove(childCoordinator: rootCoordinator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func toPresentable() -> UIViewController {
|
func toPresentable() -> UIViewController {
|
||||||
return self.navigationRouter.toPresentable()
|
return self.navigationRouter.toPresentable()
|
||||||
@@ -99,7 +117,7 @@ final class SpaceMembersCoordinator: SpaceMembersCoordinatorType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func createSpaceMemberDetailCoordinator(with member: MXRoomMember) -> SpaceMemberDetailCoordinator {
|
private func createSpaceMemberDetailCoordinator(with member: MXRoomMember) -> SpaceMemberDetailCoordinator {
|
||||||
let parameters = SpaceMemberDetailCoordinatorParameters(userSessionsService: self.parameters.userSessionsService, member: member, session: self.parameters.session, spaceId: self.parameters.spaceId)
|
let parameters = SpaceMemberDetailCoordinatorParameters(userSessionsService: self.parameters.userSessionsService, member: member, session: self.parameters.session, spaceId: self.parameters.spaceId, showCancelMenuItem: false)
|
||||||
let coordinator = SpaceMemberDetailCoordinator(parameters: parameters)
|
let coordinator = SpaceMemberDetailCoordinator(parameters: parameters)
|
||||||
coordinator.delegate = self
|
coordinator.delegate = self
|
||||||
return coordinator
|
return coordinator
|
||||||
@@ -158,6 +176,7 @@ extension SpaceMembersCoordinator: SpaceMemberListCoordinatorDelegate {
|
|||||||
coordinator.delegate = self
|
coordinator.delegate = self
|
||||||
coordinator.start()
|
coordinator.start()
|
||||||
self.childCoordinators.append(coordinator)
|
self.childCoordinators.append(coordinator)
|
||||||
|
self.navigationRouter.present(coordinator.toPresentable(), animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,9 @@ final class ExploreRoomCoordinator: NSObject, ExploreRoomCoordinatorType {
|
|||||||
|
|
||||||
// MARK: - Setup
|
// MARK: - Setup
|
||||||
|
|
||||||
init(session: MXSession, spaceId: String) {
|
init(session: MXSession, spaceId: String,
|
||||||
self.navigationRouter = NavigationRouter(navigationController: RiotNavigationController())
|
navigationRouter: NavigationRouterType = NavigationRouter(navigationController: RiotNavigationController())) {
|
||||||
|
self.navigationRouter = navigationRouter
|
||||||
self.session = session
|
self.session = session
|
||||||
self.spaceId = spaceId
|
self.spaceId = spaceId
|
||||||
self.spaceIdStack = [spaceId]
|
self.spaceIdStack = [spaceId]
|
||||||
@@ -65,7 +66,13 @@ final class ExploreRoomCoordinator: NSObject, ExploreRoomCoordinatorType {
|
|||||||
self.add(childCoordinator: rootCoordinator)
|
self.add(childCoordinator: rootCoordinator)
|
||||||
self.currentExploreRoomCoordinator = rootCoordinator
|
self.currentExploreRoomCoordinator = rootCoordinator
|
||||||
|
|
||||||
self.navigationRouter.setRootModule(rootCoordinator)
|
if self.navigationRouter.modules.isEmpty {
|
||||||
|
self.navigationRouter.setRootModule(rootCoordinator)
|
||||||
|
} else {
|
||||||
|
self.navigationRouter.push(rootCoordinator, animated: true) {
|
||||||
|
self.remove(childCoordinator: rootCoordinator)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toPresentable() -> UIViewController {
|
func toPresentable() -> UIViewController {
|
||||||
@@ -75,13 +82,17 @@ final class ExploreRoomCoordinator: NSObject, ExploreRoomCoordinatorType {
|
|||||||
// MARK: - Private methods
|
// MARK: - Private methods
|
||||||
|
|
||||||
private func pushSpace(with item: SpaceExploreRoomListItemViewData) {
|
private func pushSpace(with item: SpaceExploreRoomListItemViewData) {
|
||||||
let coordinator = self.createShowSpaceExploreRoomCoordinator(session: self.session, spaceId: item.childInfo.childRoomId, spaceName: item.childInfo.name)
|
pushSpace(with: item.childInfo.childRoomId, name: item.childInfo.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pushSpace(with spaceId: String, name: String?) {
|
||||||
|
let coordinator = self.createShowSpaceExploreRoomCoordinator(session: self.session, spaceId: spaceId, spaceName: name)
|
||||||
coordinator.start()
|
coordinator.start()
|
||||||
|
|
||||||
self.add(childCoordinator: coordinator)
|
self.add(childCoordinator: coordinator)
|
||||||
self.currentExploreRoomCoordinator = coordinator
|
self.currentExploreRoomCoordinator = coordinator
|
||||||
|
|
||||||
self.spaceIdStack.append(item.childInfo.childRoomId)
|
self.spaceIdStack.append(spaceId)
|
||||||
|
|
||||||
self.navigationRouter.push(coordinator.toPresentable(), animated: true) {
|
self.navigationRouter.push(coordinator.toPresentable(), animated: true) {
|
||||||
self.remove(childCoordinator: coordinator)
|
self.remove(childCoordinator: coordinator)
|
||||||
@@ -174,7 +185,7 @@ final class ExploreRoomCoordinator: NSObject, ExploreRoomCoordinatorType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func pushInviteScreen(forRoomWithId roomId: String) {
|
private func presentInviteScreen(forRoomWithId roomId: String) {
|
||||||
guard let room = session.room(withRoomId: roomId) else {
|
guard let room = session.room(withRoomId: roomId) else {
|
||||||
MXLog.error("[ExploreRoomCoordinator] pushInviteScreen: room not found.")
|
MXLog.error("[ExploreRoomCoordinator] pushInviteScreen: room not found.")
|
||||||
return
|
return
|
||||||
@@ -183,7 +194,44 @@ final class ExploreRoomCoordinator: NSObject, ExploreRoomCoordinatorType {
|
|||||||
let coordinator = ContactsPickerCoordinator(session: session, room: room, initialSearchText: nil, actualParticipants: nil, invitedParticipants: nil, userParticipant: nil, navigationRouter: navigationRouter)
|
let coordinator = ContactsPickerCoordinator(session: session, room: room, initialSearchText: nil, actualParticipants: nil, invitedParticipants: nil, userParticipant: nil, navigationRouter: navigationRouter)
|
||||||
coordinator.delegate = self
|
coordinator.delegate = self
|
||||||
coordinator.start()
|
coordinator.start()
|
||||||
childCoordinators.append(coordinator)
|
self.add(childCoordinator: coordinator)
|
||||||
|
self.navigationRouter.present(coordinator, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
private func showSpaceSettings(of childInfo: MXSpaceChildInfo) {
|
||||||
|
let coordinator = SpaceSettingsModalCoordinator(parameters: SpaceSettingsModalCoordinatorParameters(session: session, spaceId: childInfo.childRoomId))
|
||||||
|
coordinator.callback = { [weak self] result in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .cancel(let spaceId), .done(let spaceId):
|
||||||
|
if spaceId != childInfo.childRoomId {
|
||||||
|
// the space has been upgraded. We need to refresh the rooms list
|
||||||
|
self.currentExploreRoomCoordinator?.reloadRooms()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.navigationRouter.dismissModule(animated: true) {
|
||||||
|
self.remove(childCoordinator: coordinator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
coordinator.start()
|
||||||
|
self.add(childCoordinator: coordinator)
|
||||||
|
self.navigationRouter.present(coordinator.toPresentable(), animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presentSettings(ofRoomWithId roomId: String) -> Bool {
|
||||||
|
guard let room = session.room(withRoomId: roomId) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let coordinator = RoomInfoCoordinator(parameters: RoomInfoCoordinatorParameters(session: session, room: room, parentSpaceId: self.spaceIdStack.last, initialSection: .settings, dismissOnCancel: true))
|
||||||
|
coordinator.delegate = self
|
||||||
|
add(childCoordinator: coordinator)
|
||||||
|
coordinator.start()
|
||||||
|
self.navigationRouter.present(coordinator.toPresentable(), animated: true)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startEditPollCoordinator(room: MXRoom, startEvent: MXEvent? = nil) {
|
private func startEditPollCoordinator(room: MXRoom, startEvent: MXEvent? = nil) {
|
||||||
@@ -229,11 +277,19 @@ extension ExploreRoomCoordinator: SpaceExploreRoomCoordinatorDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func spaceExploreRoomCoordinator(_ coordinator: SpaceExploreRoomCoordinatorType, openSettingsOf item: SpaceExploreRoomListItemViewData) {
|
func spaceExploreRoomCoordinator(_ coordinator: SpaceExploreRoomCoordinatorType, openSettingsOf item: SpaceExploreRoomListItemViewData) {
|
||||||
self.navigateTo(roomWith: item.childInfo.childRoomId, showSettingsInitially: true, animated: true)
|
if item.childInfo.roomType == .space {
|
||||||
|
if #available(iOS 14, *) {
|
||||||
|
self.showSpaceSettings(of: item.childInfo)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !presentSettings(ofRoomWithId: item.childInfo.childRoomId) {
|
||||||
|
self.navigateTo(roomWith: item.childInfo.childRoomId, showSettingsInitially: true, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func spaceExploreRoomCoordinator(_ coordinator: SpaceExploreRoomCoordinatorType, inviteTo item: SpaceExploreRoomListItemViewData) {
|
func spaceExploreRoomCoordinator(_ coordinator: SpaceExploreRoomCoordinatorType, inviteTo item: SpaceExploreRoomListItemViewData) {
|
||||||
self.pushInviteScreen(forRoomWithId: item.childInfo.childRoomId)
|
self.presentInviteScreen(forRoomWithId: item.childInfo.childRoomId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,3 +463,33 @@ extension ExploreRoomCoordinator: ContactsPickerCoordinatorDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - RoomInfoCoordinatorDelegate
|
||||||
|
extension ExploreRoomCoordinator: RoomInfoCoordinatorDelegate {
|
||||||
|
func roomInfoCoordinatorDidComplete(_ coordinator: RoomInfoCoordinatorType) {
|
||||||
|
self.navigationRouter.dismissModule(animated: true) {
|
||||||
|
self.remove(childCoordinator: coordinator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, didRequestMentionForMember member: MXRoomMember) {
|
||||||
|
// Do nothing in this case
|
||||||
|
}
|
||||||
|
|
||||||
|
func roomInfoCoordinatorDidLeaveRoom(_ coordinator: RoomInfoCoordinatorType) {
|
||||||
|
self.currentExploreRoomCoordinator?.reloadRooms()
|
||||||
|
|
||||||
|
self.navigationRouter.dismissModule(animated: true) {
|
||||||
|
self.remove(childCoordinator: coordinator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, didReplaceRoomWithReplacementId roomId: String) {
|
||||||
|
self.currentExploreRoomCoordinator?.reloadRooms()
|
||||||
|
|
||||||
|
self.navigationRouter.dismissModule(animated: true) {
|
||||||
|
self.remove(childCoordinator: coordinator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ struct SpaceAvatarImage: View {
|
|||||||
Image(uiImage: image)
|
Image(uiImage: image)
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: CGFloat(size.rawValue), height: CGFloat(size.rawValue))
|
.frame(width: CGFloat(size.rawValue), height: CGFloat(size.rawValue))
|
||||||
.clipShape(Circle())
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: displayName, perform: { value in
|
.onChange(of: displayName, perform: { value in
|
||||||
|
|||||||
@@ -16,6 +16,20 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
extension ThemableTextField {
|
||||||
|
func showClearButton(text: Binding<String>, alignement: VerticalAlignment = .center) -> some View {
|
||||||
|
return modifier(ClearViewModifier(alignment: alignement, text: text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
extension ThemableTextEditor {
|
||||||
|
func showClearButton(text: Binding<String>, alignement: VerticalAlignment = .top) -> some View {
|
||||||
|
return modifier(ClearViewModifier(alignment: alignement, text: text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// `ClearViewModifier` aims to add a clear button (e.g. `x` button) on the right side of any text editing view
|
/// `ClearViewModifier` aims to add a clear button (e.g. `x` button) on the right side of any text editing view
|
||||||
@available(iOS 14.0, *)
|
@available(iOS 14.0, *)
|
||||||
struct ClearViewModifier: ViewModifier
|
struct ClearViewModifier: ViewModifier
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ struct RoundedBorderTextEditor: View {
|
|||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||||
|
@Environment(\.isEnabled) private var isEnabled
|
||||||
|
|
||||||
// MARK: Setup
|
// MARK: Setup
|
||||||
|
|
||||||
@@ -73,18 +74,32 @@ struct RoundedBorderTextEditor: View {
|
|||||||
.foregroundColor(theme.colors.tertiaryContent)
|
.foregroundColor(theme.colors.tertiaryContent)
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
}
|
}
|
||||||
ThemableTextEditor(text: $text, onEditingChanged: { edit in
|
if isEnabled {
|
||||||
self.editing = edit
|
ThemableTextEditor(text: $text, onEditingChanged: { edit in
|
||||||
onEditingChanged?(edit)
|
self.editing = edit
|
||||||
})
|
onEditingChanged?(edit)
|
||||||
.modifier(ClearViewModifier(alignment: .top, text: $text))
|
})
|
||||||
// Found no good solution here. Hidding next button for the moment
|
.showClearButton(text: $text)
|
||||||
// .modifier(NextViewModifier(alignment: .bottomTrailing, isEditing: $editing))
|
// Found no good solution here. Hidding next button for the moment
|
||||||
.padding(EdgeInsets(top: 2, leading: 6, bottom: 0, trailing: 0))
|
// .modifier(NextViewModifier(alignment: .bottomTrailing, isEditing: $editing))
|
||||||
.onChange(of: text, perform: { newText in
|
.padding(EdgeInsets(top: 2, leading: 6, bottom: 0, trailing: 0))
|
||||||
onTextChanged?(newText)
|
.onChange(of: text, perform: { newText in
|
||||||
})
|
onTextChanged?(newText)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
ThemableTextEditor(text: $text, onEditingChanged: { edit in
|
||||||
|
self.editing = edit
|
||||||
|
onEditingChanged?(edit)
|
||||||
|
})
|
||||||
|
.padding(EdgeInsets(top: 2, leading: 6, bottom: 0, trailing: 6))
|
||||||
|
.onChange(of: text, perform: { newText in
|
||||||
|
onTextChanged?(newText)
|
||||||
|
})
|
||||||
|
.opacity(0.5)
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.background(RoundedRectangle(cornerRadius: 8).fill(theme.colors.background))
|
||||||
.overlay(RoundedRectangle(cornerRadius: 8)
|
.overlay(RoundedRectangle(cornerRadius: 8)
|
||||||
.stroke(editing ? theme.colors.accent : (error == nil ? theme.colors.quinaryContent : theme.colors.alert), lineWidth: editing || error != nil ? 2 : 1))
|
.stroke(editing ? theme.colors.accent : (error == nil ? theme.colors.quinaryContent : theme.colors.alert), lineWidth: editing || error != nil ? 2 : 1))
|
||||||
.frame(height: textMaxHeight)
|
.frame(height: textMaxHeight)
|
||||||
@@ -108,18 +123,19 @@ struct ThemableTextEditor_Previews: PreviewProvider {
|
|||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
|
|
||||||
Group {
|
Group {
|
||||||
VStack(alignment: .center, spacing: 40) {
|
sampleView.theme(.light).preferredColorScheme(.light)
|
||||||
RoundedBorderTextEditor(title: "A title", placeHolder: "A placeholder", text: .constant(""), error: .constant(nil))
|
sampleView.theme(.dark).preferredColorScheme(.dark)
|
||||||
RoundedBorderTextEditor(placeHolder: "A placeholder", text: .constant("Some text"), error: .constant(nil))
|
|
||||||
RoundedBorderTextEditor(title: "A title", placeHolder: "A placeholder", text: .constant("Some very long text used to check overlapping with the delete button"), error: .constant("Some error text"))
|
|
||||||
}
|
|
||||||
VStack(alignment: .center, spacing: 40) {
|
|
||||||
RoundedBorderTextEditor(title: "A title", placeHolder: "A placeholder", text: .constant(""), error: .constant(nil))
|
|
||||||
RoundedBorderTextEditor(placeHolder: "A placeholder", text: .constant("Some text"), error: .constant(nil))
|
|
||||||
RoundedBorderTextEditor(title: "A title", placeHolder: "A placeholder", text: .constant("Some text"), error: .constant("Some error text"))
|
|
||||||
}
|
|
||||||
.theme(.dark).preferredColorScheme(.dark)
|
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static var sampleView: some View {
|
||||||
|
VStack(alignment: .center, spacing: 40) {
|
||||||
|
RoundedBorderTextEditor(title: "A title", placeHolder: "A placeholder", text: .constant(""), error: .constant(nil))
|
||||||
|
RoundedBorderTextEditor(placeHolder: "A placeholder", text: .constant("Some text"), error: .constant(nil))
|
||||||
|
RoundedBorderTextEditor(title: "A title", placeHolder: "A placeholder", text: .constant("Some very long text used to check overlapping with the delete button"), error: .constant("Some error text"))
|
||||||
|
RoundedBorderTextEditor(title: "A title", placeHolder: "A placeholder", text: .constant("Some very long text used to check overlapping with the delete button"), error: .constant("Some error text"))
|
||||||
|
.disabled(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,10 +39,19 @@ struct RoundedBorderTextField: View {
|
|||||||
@State private var editing = false
|
@State private var editing = false
|
||||||
|
|
||||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||||
|
@Environment(\.isEnabled) private var isEnabled
|
||||||
|
|
||||||
// MARK: Setup
|
// MARK: Setup
|
||||||
|
|
||||||
init(title: String? = nil, placeHolder: String, text: Binding<String>, footerText: Binding<String?> = .constant(nil), isError: Binding<Bool> = .constant(false), isFirstResponder: Bool = false, configuration: UIKitTextInputConfiguration = UIKitTextInputConfiguration(), onTextChanged: ((String) -> Void)? = nil, onEditingChanged: ((Bool) -> Void)? = nil) {
|
init(title: String? = nil,
|
||||||
|
placeHolder: String,
|
||||||
|
text: Binding<String>,
|
||||||
|
footerText: Binding<String?> = .constant(nil),
|
||||||
|
isError: Binding<Bool> = .constant(false),
|
||||||
|
isFirstResponder: Bool = false,
|
||||||
|
configuration: UIKitTextInputConfiguration = UIKitTextInputConfiguration(),
|
||||||
|
onTextChanged: ((String) -> Void)? = nil,
|
||||||
|
onEditingChanged: ((Bool) -> Void)? = nil) {
|
||||||
self.title = title
|
self.title = title
|
||||||
self.placeHolder = placeHolder
|
self.placeHolder = placeHolder
|
||||||
self._text = text
|
self._text = text
|
||||||
@@ -72,18 +81,33 @@ struct RoundedBorderTextField: View {
|
|||||||
.foregroundColor(theme.colors.tertiaryContent)
|
.foregroundColor(theme.colors.tertiaryContent)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
ThemableTextField(placeholder: "", text: $text, configuration: configuration, onEditingChanged: { edit in
|
if isEnabled {
|
||||||
self.editing = edit
|
ThemableTextField(placeholder: "", text: $text, configuration: configuration, onEditingChanged: { edit in
|
||||||
onEditingChanged?(edit)
|
self.editing = edit
|
||||||
})
|
onEditingChanged?(edit)
|
||||||
.makeFirstResponder(isFirstResponder)
|
})
|
||||||
.onChange(of: text, perform: { newText in
|
.makeFirstResponder(isFirstResponder)
|
||||||
onTextChanged?(newText)
|
.showClearButton(text: $text)
|
||||||
})
|
.onChange(of: text, perform: { newText in
|
||||||
.frame(height: 30)
|
onTextChanged?(newText)
|
||||||
.modifier(ClearViewModifier(alignment: .center, text: $text))
|
})
|
||||||
|
.frame(height: 30)
|
||||||
|
} else {
|
||||||
|
ThemableTextField(placeholder: "", text: $text, configuration: configuration, onEditingChanged: { edit in
|
||||||
|
self.editing = edit
|
||||||
|
onEditingChanged?(edit)
|
||||||
|
})
|
||||||
|
.makeFirstResponder(isFirstResponder)
|
||||||
|
.onChange(of: text, perform: { newText in
|
||||||
|
onTextChanged?(newText)
|
||||||
|
})
|
||||||
|
.frame(height: 30)
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
.opacity(0.5)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: text.isEmpty ? 8 : 0))
|
.padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: text.isEmpty ? 8 : 0))
|
||||||
|
.background(RoundedRectangle(cornerRadius: 8).fill(theme.colors.background))
|
||||||
.overlay(RoundedRectangle(cornerRadius: 8)
|
.overlay(RoundedRectangle(cornerRadius: 8)
|
||||||
.stroke(editing ? theme.colors.accent : (footerText != nil && isError ? theme.colors.alert : theme.colors.quinaryContent), lineWidth: editing || (footerText != nil && isError) ? 2 : 1))
|
.stroke(editing ? theme.colors.accent : (footerText != nil && isError ? theme.colors.alert : theme.colors.quinaryContent), lineWidth: editing || (footerText != nil && isError) ? 2 : 1))
|
||||||
|
|
||||||
@@ -107,20 +131,20 @@ struct TextFieldWithError_Previews: PreviewProvider {
|
|||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
|
|
||||||
Group {
|
Group {
|
||||||
VStack(alignment: .center, spacing: 40) {
|
sampleView.theme(.light).preferredColorScheme(.light)
|
||||||
RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant(""), footerText: .constant(nil), isError: .constant(false))
|
sampleView.theme(.dark).preferredColorScheme(.dark)
|
||||||
RoundedBorderTextField(placeHolder: "A placeholder", text: .constant("Some text"), footerText: .constant(nil), isError: .constant(false))
|
|
||||||
RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant("Some very long text used to check overlapping with the delete button"), footerText: .constant("Some error text"), isError: .constant(true))
|
|
||||||
RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant("Some very long text used to check overlapping with the delete button"), footerText: .constant("Some normal text"), isError: .constant(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .center, spacing: 20) {
|
|
||||||
RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant(""), footerText: .constant(nil), isError: .constant(false))
|
|
||||||
RoundedBorderTextField(placeHolder: "A placeholder", text: .constant("Some text"), footerText: .constant(nil), isError: .constant(false))
|
|
||||||
RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant("Some very long text used to check overlapping with the delete button"), footerText: .constant("Some error text"), isError: .constant(true))
|
|
||||||
RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant("Some very long text used to check overlapping with the delete button"), footerText: .constant("Some normal text"), isError: .constant(false))
|
|
||||||
}.theme(.dark).preferredColorScheme(.dark)
|
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static var sampleView: some View {
|
||||||
|
VStack(alignment: .center, spacing: 20) {
|
||||||
|
RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant(""), footerText: .constant(nil), isError: .constant(false))
|
||||||
|
RoundedBorderTextField(placeHolder: "A placeholder", text: .constant("Some text"), footerText: .constant(nil), isError: .constant(false))
|
||||||
|
RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant("Some very long text used to check overlapping with the delete button"), footerText: .constant("Some error text"), isError: .constant(true))
|
||||||
|
RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant("Some very long text used to check overlapping with the delete button"), footerText: .constant("Some normal text"), isError: .constant(false))
|
||||||
|
RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant("Some very long text used to check overlapping with the delete button"), footerText: .constant("Some normal text"), isError: .constant(false))
|
||||||
|
.disabled(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ final class RoomAccessCoordinator: Coordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func createRoomAccessTypeCoordinator() -> RoomAccessTypeChooserCoordinator {
|
private func createRoomAccessTypeCoordinator() -> RoomAccessTypeChooserCoordinator {
|
||||||
let coordinator: RoomAccessTypeChooserCoordinator = RoomAccessTypeChooserCoordinator(parameters: RoomAccessTypeChooserCoordinatorParameters(roomId: parameters.room.roomId, session: parameters.room.mxSession))
|
let coordinator: RoomAccessTypeChooserCoordinator = RoomAccessTypeChooserCoordinator(parameters: RoomAccessTypeChooserCoordinatorParameters(roomId: parameters.room.roomId, allowsRoomUpgrade: parameters.allowsRoomUpgrade, session: parameters.room.mxSession))
|
||||||
coordinator.callback = { [weak self] result in
|
coordinator.callback = { [weak self] result in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
|||||||
+9
-2
@@ -34,6 +34,7 @@ final class RoomAccessCoordinatorBridgePresenter: NSObject {
|
|||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
private let room: MXRoom
|
private let room: MXRoom
|
||||||
|
private let allowsRoomUpgrade: Bool
|
||||||
private var coordinator: RoomAccessCoordinator?
|
private var coordinator: RoomAccessCoordinator?
|
||||||
|
|
||||||
// MARK: Public
|
// MARK: Public
|
||||||
@@ -42,16 +43,22 @@ final class RoomAccessCoordinatorBridgePresenter: NSObject {
|
|||||||
|
|
||||||
// MARK: - Setup
|
// MARK: - Setup
|
||||||
|
|
||||||
init(room: MXRoom) {
|
init(room: MXRoom,
|
||||||
|
allowsRoomUpgrade: Bool) {
|
||||||
self.room = room
|
self.room = room
|
||||||
|
self.allowsRoomUpgrade = allowsRoomUpgrade
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
convenience init(room: MXRoom) {
|
||||||
|
self.init(room: room, allowsRoomUpgrade: true)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
|
|
||||||
func present(from viewController: UIViewController, animated: Bool) {
|
func present(from viewController: UIViewController, animated: Bool) {
|
||||||
let navigationRouter = NavigationRouter()
|
let navigationRouter = NavigationRouter()
|
||||||
let coordinator = RoomAccessCoordinator(parameters: RoomAccessCoordinatorParameters(room: room, navigationRouter: navigationRouter))
|
let coordinator = RoomAccessCoordinator(parameters: RoomAccessCoordinatorParameters(room: room, allowsRoomUpgrade: allowsRoomUpgrade, navigationRouter: navigationRouter))
|
||||||
coordinator.callback = { [weak self] result in
|
coordinator.callback = { [weak self] result in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
|||||||
@@ -23,13 +23,18 @@ struct RoomAccessCoordinatorParameters {
|
|||||||
|
|
||||||
/// The Matrix room
|
/// The Matrix room
|
||||||
let room: MXRoom
|
let room: MXRoom
|
||||||
|
|
||||||
|
/// Set this value to false if you want to avoid room to be upgraded
|
||||||
|
let allowsRoomUpgrade: Bool
|
||||||
|
|
||||||
/// The navigation router that manage physical navigation
|
/// The navigation router that manage physical navigation
|
||||||
let navigationRouter: NavigationRouterType
|
let navigationRouter: NavigationRouterType
|
||||||
|
|
||||||
init(room: MXRoom,
|
init(room: MXRoom,
|
||||||
|
allowsRoomUpgrade: Bool = true,
|
||||||
navigationRouter: NavigationRouterType? = nil) {
|
navigationRouter: NavigationRouterType? = nil) {
|
||||||
self.room = room
|
self.room = room
|
||||||
|
self.allowsRoomUpgrade = allowsRoomUpgrade
|
||||||
self.navigationRouter = navigationRouter ?? NavigationRouter(navigationController: RiotNavigationController())
|
self.navigationRouter = navigationRouter ?? NavigationRouter(navigationController: RiotNavigationController())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -18,6 +18,7 @@ import SwiftUI
|
|||||||
|
|
||||||
struct RoomAccessTypeChooserCoordinatorParameters {
|
struct RoomAccessTypeChooserCoordinatorParameters {
|
||||||
let roomId: String
|
let roomId: String
|
||||||
|
let allowsRoomUpgrade: Bool
|
||||||
let session: MXSession
|
let session: MXSession
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ final class RoomAccessTypeChooserCoordinator: Coordinator, Presentable {
|
|||||||
@available(iOS 14.0, *)
|
@available(iOS 14.0, *)
|
||||||
init(parameters: RoomAccessTypeChooserCoordinatorParameters) {
|
init(parameters: RoomAccessTypeChooserCoordinatorParameters) {
|
||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
let viewModel = RoomAccessTypeChooserViewModel(roomAccessTypeChooserService: RoomAccessTypeChooserService(roomId: parameters.roomId, session: parameters.session))
|
let viewModel = RoomAccessTypeChooserViewModel(roomAccessTypeChooserService: RoomAccessTypeChooserService(roomId: parameters.roomId, allowsRoomUpgrade: parameters.allowsRoomUpgrade, session: parameters.session))
|
||||||
let room = parameters.session.room(withRoomId: parameters.roomId)
|
let room = parameters.session.room(withRoomId: parameters.roomId)
|
||||||
let view = RoomAccessTypeChooser(viewModel: viewModel.context, roomName: room?.displayName ?? "")
|
let view = RoomAccessTypeChooser(viewModel: viewModel.context, roomName: room?.displayName ?? "")
|
||||||
roomAccessTypeChooserViewModel = viewModel
|
roomAccessTypeChooserViewModel = viewModel
|
||||||
|
|||||||
+4
-2
@@ -26,6 +26,7 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
|
|||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
private let roomId: String
|
private let roomId: String
|
||||||
|
private let allowsRoomUpgrade: Bool
|
||||||
private let session: MXSession
|
private let session: MXSession
|
||||||
private var replacementRoom: MXRoom?
|
private var replacementRoom: MXRoom?
|
||||||
private var didBuildSpaceGraphObserver: Any?
|
private var didBuildSpaceGraphObserver: Any?
|
||||||
@@ -63,8 +64,9 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
|
|||||||
|
|
||||||
// MARK: - Setup
|
// MARK: - Setup
|
||||||
|
|
||||||
init(roomId: String, session: MXSession) {
|
init(roomId: String, allowsRoomUpgrade: Bool, session: MXSession) {
|
||||||
self.roomId = roomId
|
self.roomId = roomId
|
||||||
|
self.allowsRoomUpgrade = allowsRoomUpgrade
|
||||||
self.session = session
|
self.session = session
|
||||||
self.currentRoomId = roomId
|
self.currentRoomId = roomId
|
||||||
self.versionOverride = session.homeserverCapabilitiesService.versionOverrideForFeature(.restricted)
|
self.versionOverride = session.homeserverCapabilitiesService.versionOverrideForFeature(.restricted)
|
||||||
@@ -144,7 +146,7 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
|
|||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func setupAccessItems() {
|
private func setupAccessItems() {
|
||||||
guard let spaceService = session.spaceService, let ancestors = spaceService.ancestorsPerRoomId[currentRoomId], !ancestors.isEmpty else {
|
guard let spaceService = session.spaceService, let ancestors = spaceService.ancestorsPerRoomId[currentRoomId], !ancestors.isEmpty, allowsRoomUpgrade || !roomUpgradeRequired else {
|
||||||
self.accessItems = [
|
self.accessItems = [
|
||||||
RoomAccessTypeChooserAccessItem(id: .private, isSelected: false, title: VectorL10n.private, detail: VectorL10n.roomAccessSettingsScreenPrivateMessage, badgeText: nil),
|
RoomAccessTypeChooserAccessItem(id: .private, isSelected: false, title: VectorL10n.private, detail: VectorL10n.roomAccessSettingsScreenPrivateMessage, badgeText: nil),
|
||||||
RoomAccessTypeChooserAccessItem(id: .public, isSelected: false, title: VectorL10n.public, detail: VectorL10n.roomAccessSettingsScreenPublicMessage, badgeText: nil),
|
RoomAccessTypeChooserAccessItem(id: .public, isSelected: false, title: VectorL10n.public, detail: VectorL10n.roomAccessSettingsScreenPublicMessage, badgeText: nil),
|
||||||
|
|||||||
+3
-3
@@ -37,13 +37,13 @@ class MatrixItemChooserRoomRestrictedAllowedParentsDataSource: MatrixItemChooser
|
|||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
let joinRuleEvent = state?.stateEvents(with: .roomJoinRules)?.last
|
let joinRuleEvent = state?.stateEvents(with: .roomJoinRules)?.last
|
||||||
let allowContent: [[String: String]] = joinRuleEvent?.wireContent["allow"] as? [[String: String]] ?? []
|
let allowContent: [[String: String]] = joinRuleEvent?.wireContent[kMXJoinRulesContentKeyAllow] as? [[String: String]] ?? []
|
||||||
self.allowedParentIds = allowContent.compactMap { allowDictionnary in
|
self.allowedParentIds = allowContent.compactMap { allowDictionnary in
|
||||||
guard let type = allowDictionnary["type"], type == "m.room_membership" else {
|
guard let type = allowDictionnary[kMXJoinRulesContentKeyType], type == kMXEventTypeStringRoomMembership else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return allowDictionnary["room_id"]
|
return allowDictionnary[kMXJoinRulesContentKeyRoomId]
|
||||||
}
|
}
|
||||||
|
|
||||||
let ancestorsId = session.spaceService.ancestorsPerRoomId[self.roomId] ?? []
|
let ancestorsId = session.spaceService.ancestorsPerRoomId[self.roomId] ?? []
|
||||||
|
|||||||
+25
@@ -18,7 +18,32 @@ import Foundation
|
|||||||
|
|
||||||
enum SpaceCreationSettingsAddressValidationStatus {
|
enum SpaceCreationSettingsAddressValidationStatus {
|
||||||
case none(_ address: String)
|
case none(_ address: String)
|
||||||
|
case current(_ address: String)
|
||||||
case valid(_ address: String)
|
case valid(_ address: String)
|
||||||
case alreadyExists(_ address: String)
|
case alreadyExists(_ address: String)
|
||||||
case invalidCharacters(_ address: String)
|
case invalidCharacters(_ address: String)
|
||||||
|
|
||||||
|
var message: String {
|
||||||
|
switch self {
|
||||||
|
case .none(let fullAddress):
|
||||||
|
return VectorL10n.spacesCreationAddressDefaultMessage(fullAddress)
|
||||||
|
case .current(let fullAddress):
|
||||||
|
return VectorL10n.spaceSettingsCurrentAddressMessage(fullAddress)
|
||||||
|
case .valid(let fullAddress):
|
||||||
|
return VectorL10n.spacesCreationAddressDefaultMessage(fullAddress)
|
||||||
|
case .alreadyExists(let fullAddress):
|
||||||
|
return VectorL10n.spacesCreationAddressAlreadyExists(fullAddress)
|
||||||
|
case .invalidCharacters(let fullAddress):
|
||||||
|
return VectorL10n.spacesCreationAddressInvalidCharacters(fullAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isValid: Bool {
|
||||||
|
switch self {
|
||||||
|
case .none, .current, .valid:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -94,7 +94,7 @@ class SpaceCreationSettingsService: SpaceCreationSettingsServiceProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateDefaultAddress() {
|
private func updateDefaultAddress() {
|
||||||
defaultAddress = roomName.toValidAliasLocalPart()
|
defaultAddress = MXTools.validAliasLocalPart(from: roomName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func validateAddress() {
|
private func validateAddress() {
|
||||||
@@ -102,7 +102,7 @@ class SpaceCreationSettingsService: SpaceCreationSettingsServiceProtocol {
|
|||||||
currentOperation = nil
|
currentOperation = nil
|
||||||
|
|
||||||
guard let userDefinedAddress = self.userDefinedAddress, !userDefinedAddress.isEmpty else {
|
guard let userDefinedAddress = self.userDefinedAddress, !userDefinedAddress.isEmpty else {
|
||||||
let fullAddress = defaultAddress.fullLocalAlias(with: session)
|
let fullAddress = MXTools.fullLocalAlias(from: defaultAddress, with: session)
|
||||||
|
|
||||||
if defaultAddress.isEmpty {
|
if defaultAddress.isEmpty {
|
||||||
addressValidationSubject.send(.none(fullAddress))
|
addressValidationSubject.send(.none(fullAddress))
|
||||||
@@ -116,7 +116,7 @@ class SpaceCreationSettingsService: SpaceCreationSettingsServiceProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func validate(_ aliasLocalPart: String) {
|
private func validate(_ aliasLocalPart: String) {
|
||||||
let fullAddress = aliasLocalPart.fullLocalAlias(with: session)
|
let fullAddress = MXTools.fullLocalAlias(from: aliasLocalPart, with: session)
|
||||||
|
|
||||||
currentOperation = MXRoomAliasAvailabilityChecker.validate(aliasLocalPart: aliasLocalPart, with: session) { [weak self] result in
|
currentOperation = MXRoomAliasAvailabilityChecker.validate(aliasLocalPart: aliasLocalPart, with: session) { [weak self] result in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|||||||
+4
-26
@@ -76,8 +76,8 @@ class SpaceCreationSettingsViewModel: SpaceCreationSettingsViewModelType, SpaceC
|
|||||||
showRoomAddress: creationParameters.showAddress,
|
showRoomAddress: creationParameters.showAddress,
|
||||||
defaultAddress: creationParameters.address ?? "",
|
defaultAddress: creationParameters.address ?? "",
|
||||||
roomNameError: nil,
|
roomNameError: nil,
|
||||||
addressMessage: addressMessage(with: validationStatus),
|
addressMessage: validationStatus.message,
|
||||||
isAddressValid: isAddressValid(with: validationStatus),
|
isAddressValid: validationStatus.isValid,
|
||||||
avatar: AvatarInput(mxContentUri: nil, matrixItemId: "", displayName: nil),
|
avatar: AvatarInput(mxContentUri: nil, matrixItemId: "", displayName: nil),
|
||||||
avatarImage: creationParameters.userSelectedAvatar,
|
avatarImage: creationParameters.userSelectedAvatar,
|
||||||
bindings: bindings)
|
bindings: bindings)
|
||||||
@@ -120,8 +120,8 @@ class SpaceCreationSettingsViewModel: SpaceCreationSettingsViewModelType, SpaceC
|
|||||||
case .updateRoomDefaultAddress(let defaultAddress):
|
case .updateRoomDefaultAddress(let defaultAddress):
|
||||||
state.defaultAddress = defaultAddress
|
state.defaultAddress = defaultAddress
|
||||||
case .updateAddressValidationStatus(let validationStatus):
|
case .updateAddressValidationStatus(let validationStatus):
|
||||||
state.addressMessage = Self.addressMessage(with: validationStatus)
|
state.addressMessage = validationStatus.message
|
||||||
state.isAddressValid = Self.isAddressValid(with: validationStatus)
|
state.isAddressValid = validationStatus.isValid
|
||||||
case .updateAvatar(let avatar):
|
case .updateAvatar(let avatar):
|
||||||
state.avatar = avatar
|
state.avatar = avatar
|
||||||
case .updateAvatarImage(let image):
|
case .updateAvatarImage(let image):
|
||||||
@@ -161,26 +161,4 @@ class SpaceCreationSettingsViewModel: SpaceCreationSettingsViewModelType, SpaceC
|
|||||||
private func pickImage(from sourceRect: CGRect) {
|
private func pickImage(from sourceRect: CGRect) {
|
||||||
callback?(.pickImage(sourceRect))
|
callback?(.pickImage(sourceRect))
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func addressMessage(with validationStatus: SpaceCreationSettingsAddressValidationStatus) -> String {
|
|
||||||
switch validationStatus {
|
|
||||||
case .none(let fullAddress):
|
|
||||||
return VectorL10n.spacesCreationAddressDefaultMessage(fullAddress)
|
|
||||||
case .valid(let fullAddress):
|
|
||||||
return VectorL10n.spacesCreationAddressDefaultMessage(fullAddress)
|
|
||||||
case .alreadyExists(let fullAddress):
|
|
||||||
return VectorL10n.spacesCreationAddressAlreadyExists(fullAddress)
|
|
||||||
case .invalidCharacters(let fullAddress):
|
|
||||||
return VectorL10n.spacesCreationAddressInvalidCharacters(fullAddress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func isAddressValid(with validationStatus: SpaceCreationSettingsAddressValidationStatus) -> Bool {
|
|
||||||
switch validationStatus {
|
|
||||||
case .none, .valid:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+183
@@ -0,0 +1,183 @@
|
|||||||
|
//
|
||||||
|
// 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 UIKit
|
||||||
|
|
||||||
|
/// Actions returned by the coordinator callback
|
||||||
|
enum SpaceSettingsModalCoordinatorAction {
|
||||||
|
case done(_ spaceId: String)
|
||||||
|
case cancel(_ spaceId: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objcMembers
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
final class SpaceSettingsModalCoordinator: Coordinator {
|
||||||
|
|
||||||
|
// MARK: - Properties
|
||||||
|
|
||||||
|
// MARK: Private
|
||||||
|
|
||||||
|
private let parameters: SpaceSettingsModalCoordinatorParameters
|
||||||
|
private var upgradedRoomId: String?
|
||||||
|
private var currentRoomId: String {
|
||||||
|
upgradedRoomId ?? parameters.spaceId
|
||||||
|
}
|
||||||
|
|
||||||
|
private var navigationRouter: NavigationRouterType {
|
||||||
|
return self.parameters.navigationRouter
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Public
|
||||||
|
|
||||||
|
// Must be used only internally
|
||||||
|
var childCoordinators: [Coordinator] = []
|
||||||
|
|
||||||
|
var callback: ((SpaceSettingsModalCoordinatorAction) -> Void)?
|
||||||
|
|
||||||
|
// MARK: - Setup
|
||||||
|
|
||||||
|
init(parameters: SpaceSettingsModalCoordinatorParameters) {
|
||||||
|
self.parameters = parameters
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public
|
||||||
|
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
MXLog.debug("[SpaceSettingsModalCoordinator] did start.")
|
||||||
|
let rootCoordinator = self.createSpaceSettingsCoordinator()
|
||||||
|
rootCoordinator.start()
|
||||||
|
|
||||||
|
self.add(childCoordinator: rootCoordinator)
|
||||||
|
|
||||||
|
if self.navigationRouter.modules.isEmpty == false {
|
||||||
|
self.navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in
|
||||||
|
self?.remove(childCoordinator: rootCoordinator)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
self.navigationRouter.setRootModule(rootCoordinator) { [weak self] in
|
||||||
|
self?.remove(childCoordinator: rootCoordinator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPresentable() -> UIViewController {
|
||||||
|
return self.navigationRouter.toPresentable()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
func pushScreen(with coordinator: Coordinator & Presentable) {
|
||||||
|
add(childCoordinator: coordinator)
|
||||||
|
|
||||||
|
self.navigationRouter.push(coordinator, animated: true, popCompletion: { [weak self] in
|
||||||
|
self?.remove(childCoordinator: coordinator)
|
||||||
|
})
|
||||||
|
|
||||||
|
coordinator.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createSpaceSettingsCoordinator() -> SpaceSettingsCoordinator {
|
||||||
|
let coordinator = SpaceSettingsCoordinator(parameters: SpaceSettingsCoordinatorParameters(session: parameters.session, spaceId: parameters.spaceId))
|
||||||
|
coordinator.completion = { [weak self] result in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .cancel:
|
||||||
|
self.callback?(.cancel(self.currentRoomId))
|
||||||
|
case .done:
|
||||||
|
self.callback?(.done(self.currentRoomId))
|
||||||
|
case .optionScreen(let optionType):
|
||||||
|
self.pushOptionScreen(ofType: optionType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return coordinator
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pushOptionScreen(ofType optionType: SpaceSettingsOptionType) {
|
||||||
|
switch optionType {
|
||||||
|
case .rooms:
|
||||||
|
exploreRooms(ofSpaceWithId: self.parameters.spaceId)
|
||||||
|
case .members:
|
||||||
|
showMembers(ofSpaceWithId: self.parameters.spaceId)
|
||||||
|
case .visibility:
|
||||||
|
showAccess(ofSpaceWithId: self.parameters.spaceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func exploreRooms(ofSpaceWithId spaceId: String) {
|
||||||
|
let coordinator = ExploreRoomCoordinator(session: parameters.session, spaceId: spaceId)
|
||||||
|
coordinator.delegate = self
|
||||||
|
add(childCoordinator: coordinator)
|
||||||
|
coordinator.start()
|
||||||
|
self.navigationRouter.present(coordinator.toPresentable(), animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showMembers(ofSpaceWithId spaceId: String) {
|
||||||
|
let coordinator = SpaceMembersCoordinator(parameters: SpaceMembersCoordinatorParameters(userSessionsService: UserSessionsService.shared, session: parameters.session, spaceId: spaceId))
|
||||||
|
coordinator.delegate = self
|
||||||
|
add(childCoordinator: coordinator)
|
||||||
|
coordinator.start()
|
||||||
|
self.navigationRouter.present(coordinator.toPresentable(), animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showAccess(ofSpaceWithId spaceId: String) {
|
||||||
|
guard let room = parameters.session.room(withRoomId: spaceId) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Needed more tests on synaose side before starting space upgrade implementation
|
||||||
|
let coordinator = RoomAccessCoordinator(parameters: RoomAccessCoordinatorParameters(room: room, allowsRoomUpgrade: false))
|
||||||
|
coordinator.callback = { [weak self] result in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .cancel(let roomId), .done(let roomId):
|
||||||
|
if roomId != spaceId {
|
||||||
|
// TODO: room has been upgraded
|
||||||
|
self.upgradedRoomId = roomId
|
||||||
|
}
|
||||||
|
|
||||||
|
self.navigationRouter.dismissModule(animated: true, completion: {
|
||||||
|
self.remove(childCoordinator: coordinator)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add(childCoordinator: coordinator)
|
||||||
|
coordinator.start()
|
||||||
|
self.navigationRouter.present(coordinator.toPresentable(), animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ExploreRoomCoordinatorDelegate
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
extension SpaceSettingsModalCoordinator: ExploreRoomCoordinatorDelegate {
|
||||||
|
func exploreRoomCoordinatorDidComplete(_ coordinator: ExploreRoomCoordinatorType) {
|
||||||
|
self.navigationRouter.dismissModule(animated: true, completion: {
|
||||||
|
self.remove(childCoordinator: coordinator)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - SpaceMembersCoordinatorDelegate
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
extension SpaceSettingsModalCoordinator: SpaceMembersCoordinatorDelegate {
|
||||||
|
func spaceMembersCoordinatorDidCancel(_ coordinator: SpaceMembersCoordinatorType) {
|
||||||
|
self.navigationRouter.dismissModule(animated: true, completion: {
|
||||||
|
self.remove(childCoordinator: coordinator)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
//
|
||||||
|
// 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 UIKit
|
||||||
|
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
@objc protocol SpaceSettingsModalCoordinatorBridgePresenterDelegate {
|
||||||
|
func spaceSettingsModalCoordinatorBridgePresenterDelegateDidCancel(_ coordinatorBridgePresenter: SpaceSettingsModalCoordinatorBridgePresenter)
|
||||||
|
func spaceSettingsModalCoordinatorBridgePresenterDelegateDidFinish(_ coordinatorBridgePresenter: SpaceSettingsModalCoordinatorBridgePresenter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SpaceSettingsModalCoordinatorBridgePresenter enables to start SpaceSettingsModalCoordinator from a view controller.
|
||||||
|
/// This bridge is used while waiting for global usage of coordinator pattern.
|
||||||
|
/// It breaks the Coordinator abstraction and it has been introduced for Objective-C compatibility (mainly for integration in legacy view controllers).
|
||||||
|
/// Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator.
|
||||||
|
@objcMembers
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
final class SpaceSettingsModalCoordinatorBridgePresenter: NSObject {
|
||||||
|
|
||||||
|
// MARK: - Properties
|
||||||
|
|
||||||
|
// MARK: Private
|
||||||
|
|
||||||
|
private let spaceId: String
|
||||||
|
private let session: MXSession
|
||||||
|
private var coordinator: SpaceSettingsModalCoordinator?
|
||||||
|
|
||||||
|
// MARK: Public
|
||||||
|
|
||||||
|
weak var delegate: SpaceSettingsModalCoordinatorBridgePresenterDelegate?
|
||||||
|
|
||||||
|
// MARK: - Setup
|
||||||
|
|
||||||
|
init(spaceId: String, session: MXSession) {
|
||||||
|
self.spaceId = spaceId
|
||||||
|
self.session = session
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public
|
||||||
|
|
||||||
|
func present(from viewController: UIViewController, animated: Bool) {
|
||||||
|
let navigationRouter = NavigationRouter()
|
||||||
|
let coordinator = SpaceSettingsModalCoordinator(parameters: SpaceSettingsModalCoordinatorParameters(session: session, spaceId: spaceId, navigationRouter: navigationRouter))
|
||||||
|
coordinator.callback = { [weak self] result in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .cancel:
|
||||||
|
self.delegate?.spaceSettingsModalCoordinatorBridgePresenterDelegateDidCancel(self)
|
||||||
|
case .done:
|
||||||
|
self.delegate?.spaceSettingsModalCoordinatorBridgePresenterDelegateDidFinish(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let presentable = coordinator.toPresentable()
|
||||||
|
presentable.presentationController?.delegate = self
|
||||||
|
navigationRouter.setRootModule(presentable)
|
||||||
|
viewController.present(navigationRouter.toPresentable(), animated: animated, completion: nil)
|
||||||
|
coordinator.start()
|
||||||
|
|
||||||
|
self.coordinator = coordinator
|
||||||
|
}
|
||||||
|
|
||||||
|
func dismiss(animated: Bool, completion: (() -> Void)?) {
|
||||||
|
guard let coordinator = self.coordinator else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
coordinator.toPresentable().dismiss(animated: animated) {
|
||||||
|
self.coordinator = nil
|
||||||
|
|
||||||
|
if let completion = completion {
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UIAdaptivePresentationControllerDelegate
|
||||||
|
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
extension SpaceSettingsModalCoordinatorBridgePresenter: UIAdaptivePresentationControllerDelegate {
|
||||||
|
|
||||||
|
func roomNotificationSettingsCoordinatorDidComplete(_ presentationController: UIPresentationController) {
|
||||||
|
self.delegate?.spaceSettingsModalCoordinatorBridgePresenterDelegateDidCancel(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
/// SpaceSettingsModalCoordinator input parameters
|
||||||
|
struct SpaceSettingsModalCoordinatorParameters {
|
||||||
|
|
||||||
|
/// The Matrix session
|
||||||
|
let session: MXSession
|
||||||
|
|
||||||
|
/// The ID of the space
|
||||||
|
let spaceId: String
|
||||||
|
|
||||||
|
/// The navigation router that manage physical navigation
|
||||||
|
let navigationRouter: NavigationRouterType
|
||||||
|
|
||||||
|
init(session: MXSession,
|
||||||
|
spaceId: String,
|
||||||
|
navigationRouter: NavigationRouterType? = nil) {
|
||||||
|
self.session = session
|
||||||
|
self.spaceId = spaceId
|
||||||
|
self.navigationRouter = navigationRouter ?? NavigationRouter(navigationController: RiotNavigationController())
|
||||||
|
}
|
||||||
|
}
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
//
|
||||||
|
// 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 SwiftUI
|
||||||
|
|
||||||
|
struct SpaceSettingsCoordinatorParameters {
|
||||||
|
let session: MXSession
|
||||||
|
let spaceId: String
|
||||||
|
}
|
||||||
|
|
||||||
|
final class SpaceSettingsCoordinator: Coordinator, Presentable {
|
||||||
|
|
||||||
|
// MARK: - Properties
|
||||||
|
|
||||||
|
// MARK: Private
|
||||||
|
|
||||||
|
private let parameters: SpaceSettingsCoordinatorParameters
|
||||||
|
private let spaceSettingsHostingController: UIViewController
|
||||||
|
private var spaceSettingsViewModel: SpaceSettingsViewModelProtocol
|
||||||
|
|
||||||
|
private lazy var singleImagePickerPresenter: SingleImagePickerPresenter = {
|
||||||
|
let presenter = SingleImagePickerPresenter(session: parameters.session)
|
||||||
|
presenter.delegate = self
|
||||||
|
return presenter
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: Public
|
||||||
|
|
||||||
|
// Must be used only internally
|
||||||
|
var childCoordinators: [Coordinator] = []
|
||||||
|
var completion: ((SpaceSettingsCoordinatorResult) -> Void)?
|
||||||
|
|
||||||
|
// MARK: - Setup
|
||||||
|
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
init(parameters: SpaceSettingsCoordinatorParameters) {
|
||||||
|
self.parameters = parameters
|
||||||
|
let viewModel = SpaceSettingsViewModel.makeSpaceSettingsViewModel(service: SpaceSettingsService(session: parameters.session, spaceId: parameters.spaceId))
|
||||||
|
let view = SpaceSettings(viewModel: viewModel.context)
|
||||||
|
.addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
|
||||||
|
spaceSettingsViewModel = viewModel
|
||||||
|
spaceSettingsHostingController = VectorHostingController(rootView: view)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
MXLog.debug("[SpaceSettingsCoordinator] did start.")
|
||||||
|
spaceSettingsViewModel.completion = { [weak self] result in
|
||||||
|
MXLog.debug("[SpaceSettingsCoordinator] SpaceSettingsViewModel did complete with result: \(result).")
|
||||||
|
guard let self = self else { return }
|
||||||
|
switch result {
|
||||||
|
case .cancel:
|
||||||
|
self.completion?(.cancel)
|
||||||
|
case .done:
|
||||||
|
self.completion?(.done)
|
||||||
|
case .optionScreen(let optionType):
|
||||||
|
self.completion?(.optionScreen(optionType))
|
||||||
|
case .pickImage(let sourceRect):
|
||||||
|
self.pickImage(from: sourceRect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPresentable() -> UIViewController {
|
||||||
|
return self.spaceSettingsHostingController
|
||||||
|
|
||||||
|
}
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private func pickImage(from sourceRect: CGRect) {
|
||||||
|
let controller = toPresentable()
|
||||||
|
let adjustedRect = controller.view.convert(sourceRect, from: nil)
|
||||||
|
singleImagePickerPresenter.present(from: controller, sourceView: controller.view, sourceRect: adjustedRect, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - SingleImagePickerPresenterDelegate
|
||||||
|
extension SpaceSettingsCoordinator: SingleImagePickerPresenterDelegate {
|
||||||
|
func singleImagePickerPresenter(_ presenter: SingleImagePickerPresenter, didSelectImageData imageData: Data, withUTI uti: MXKUTI?) {
|
||||||
|
spaceSettingsViewModel.updateAvatarImage(with: UIImage(data: imageData))
|
||||||
|
presenter.dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleImagePickerPresenterDidCancel(_ presenter: SingleImagePickerPresenter) {
|
||||||
|
presenter.dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Using an enum for the screen allows you define the different state cases with
|
||||||
|
/// the relevant associated data for each case.
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
enum MockSpaceSettingsScreenState: MockScreenState, CaseIterable {
|
||||||
|
// A case for each state you want to represent
|
||||||
|
// with specific, minimal associated data that will allow you
|
||||||
|
// mock that screen.
|
||||||
|
case visibility(SpaceSettingsVisibility)
|
||||||
|
case notEditable
|
||||||
|
|
||||||
|
/// The associated screen
|
||||||
|
var screenType: Any.Type {
|
||||||
|
SpaceSettings.self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A list of screen state definitions
|
||||||
|
static var allCases: [MockSpaceSettingsScreenState] {
|
||||||
|
SpaceSettingsVisibility.allCases.map(MockSpaceSettingsScreenState.visibility)
|
||||||
|
+ [.notEditable]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate the view struct for the screen state.
|
||||||
|
var screenView: ([Any], AnyView) {
|
||||||
|
let service: MockSpaceSettingsService
|
||||||
|
switch self {
|
||||||
|
case .visibility(let visibility):
|
||||||
|
let roomProperties = SpaceSettingsRoomProperties(
|
||||||
|
name: "Space Name",
|
||||||
|
topic: "Sapce topic",
|
||||||
|
address: nil,
|
||||||
|
avatarUrl: nil,
|
||||||
|
visibility: visibility,
|
||||||
|
allowedParentIds: [],
|
||||||
|
isAvatarEditable: true,
|
||||||
|
isNameEditable: true,
|
||||||
|
isTopicEditable: true,
|
||||||
|
isAddressEditable: true,
|
||||||
|
isAccessEditable: true)
|
||||||
|
service = MockSpaceSettingsService(roomProperties: roomProperties)
|
||||||
|
case .notEditable:
|
||||||
|
let roomProperties = SpaceSettingsRoomProperties(
|
||||||
|
name: "Space Name",
|
||||||
|
topic: "Sapce topic",
|
||||||
|
address: nil,
|
||||||
|
avatarUrl: nil,
|
||||||
|
visibility: .public,
|
||||||
|
allowedParentIds: [],
|
||||||
|
isAvatarEditable: false,
|
||||||
|
isNameEditable: false,
|
||||||
|
isTopicEditable: false,
|
||||||
|
isAddressEditable: false,
|
||||||
|
isAccessEditable: false)
|
||||||
|
service = MockSpaceSettingsService(roomProperties: roomProperties)
|
||||||
|
}
|
||||||
|
let viewModel = SpaceSettingsViewModel.makeSpaceSettingsViewModel(service: service)
|
||||||
|
|
||||||
|
// can simulate service and viewModel actions here if needs be.
|
||||||
|
|
||||||
|
return (
|
||||||
|
[service, viewModel],
|
||||||
|
AnyView(SpaceSettings(viewModel: viewModel.context)
|
||||||
|
.addDependency(MockAvatarService.example))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+474
@@ -0,0 +1,474 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
import Combine
|
||||||
|
import MatrixSDK
|
||||||
|
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
class SpaceSettingsService: SpaceSettingsServiceProtocol {
|
||||||
|
|
||||||
|
// MARK: - Properties
|
||||||
|
|
||||||
|
var userDefinedAddress: String? {
|
||||||
|
didSet {
|
||||||
|
validateAddress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Private
|
||||||
|
|
||||||
|
private let session: MXSession
|
||||||
|
private var roomState: MXRoomState? {
|
||||||
|
didSet {
|
||||||
|
updateRoomProperties()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private let room: MXRoom?
|
||||||
|
private var roomEventListener: Any?
|
||||||
|
|
||||||
|
private var publicAddress: String? {
|
||||||
|
didSet {
|
||||||
|
validateAddress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var defaultAddress: String {
|
||||||
|
didSet {
|
||||||
|
validateAddress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Public
|
||||||
|
|
||||||
|
var displayName: String? {
|
||||||
|
room?.displayName
|
||||||
|
}
|
||||||
|
|
||||||
|
private(set) var spaceId: String
|
||||||
|
private(set) var roomProperties: SpaceSettingsRoomProperties? {
|
||||||
|
didSet {
|
||||||
|
roomPropertiesSubject.send(roomProperties)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private(set) var isLoadingSubject: CurrentValueSubject<Bool, Never>
|
||||||
|
private(set) var roomPropertiesSubject: CurrentValueSubject<SpaceSettingsRoomProperties?, Never>
|
||||||
|
private(set) var showPostProcessAlert: CurrentValueSubject<Bool, Never>
|
||||||
|
|
||||||
|
private(set) var addressValidationSubject: CurrentValueSubject<SpaceCreationSettingsAddressValidationStatus, Never>
|
||||||
|
var isAddressValid: Bool {
|
||||||
|
switch addressValidationSubject.value {
|
||||||
|
case .none, .valid:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentOperation: MXHTTPOperation?
|
||||||
|
private var addressValidationOperation: MXHTTPOperation?
|
||||||
|
|
||||||
|
private lazy var mediaUploader: MXMediaLoader = MXMediaManager.prepareUploader(withMatrixSession: session, initialRange: 0, andRange: 1.0)
|
||||||
|
|
||||||
|
// MARK: - Setup
|
||||||
|
|
||||||
|
init(session: MXSession, spaceId: String) {
|
||||||
|
self.session = session
|
||||||
|
self.spaceId = spaceId
|
||||||
|
self.room = session.room(withRoomId: spaceId)
|
||||||
|
self.isLoadingSubject = CurrentValueSubject(false)
|
||||||
|
self.showPostProcessAlert = CurrentValueSubject(false)
|
||||||
|
self.roomPropertiesSubject = CurrentValueSubject(self.roomProperties)
|
||||||
|
self.addressValidationSubject = CurrentValueSubject(.none("#"))
|
||||||
|
self.defaultAddress = ""
|
||||||
|
|
||||||
|
readRoomState()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if let roomEventListener = self.roomEventListener {
|
||||||
|
self.room?.removeListener(roomEventListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentOperation?.cancel()
|
||||||
|
addressValidationOperation?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public
|
||||||
|
|
||||||
|
func addressDidChange(_ newValue: String) {
|
||||||
|
userDefinedAddress = newValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private func readRoomState() {
|
||||||
|
isLoadingSubject.send(true)
|
||||||
|
self.room?.state { [weak self] roomState in
|
||||||
|
self?.roomState = roomState
|
||||||
|
self?.isLoadingSubject.send(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
roomEventListener = self.room?.listen(toEvents: { [weak self] event, direction, state in
|
||||||
|
self?.room?.state({ [weak self] roomState in
|
||||||
|
self?.roomState = roomState
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private func visibility(with roomState: MXRoomState) -> SpaceSettingsVisibility {
|
||||||
|
switch roomState.joinRule {
|
||||||
|
case .public:
|
||||||
|
return .public
|
||||||
|
case .restricted:
|
||||||
|
return .restricted
|
||||||
|
default:
|
||||||
|
return .private
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func allowedParentIds(with roomState: MXRoomState) -> [String] {
|
||||||
|
var allowedParentIds: [String] = []
|
||||||
|
if roomState.joinRule == .restricted, let joinRuleEvent = roomState.stateEvents(with: .roomJoinRules)?.last {
|
||||||
|
let allowContent: [[String: String]] = joinRuleEvent.wireContent[kMXJoinRulesContentKeyAllow] as? [[String: String]] ?? []
|
||||||
|
allowedParentIds = allowContent.compactMap { allowDictionnary in
|
||||||
|
guard let type = allowDictionnary[kMXJoinRulesContentKeyType], type == kMXEventTypeStringRoomMembership else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowDictionnary[kMXJoinRulesContentKeyRoomId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allowedParentIds
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isField(ofType notification: String, editableWith powerLevels: MXRoomPowerLevels?) -> Bool {
|
||||||
|
guard let powerLevels = powerLevels else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: self.session.myUserId)
|
||||||
|
return userPowerLevel >= powerLevels.minimumPowerLevel(forNotifications: notification, defaultPower: powerLevels.stateDefault)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func validateAddress() {
|
||||||
|
addressValidationOperation?.cancel()
|
||||||
|
addressValidationOperation = nil
|
||||||
|
|
||||||
|
guard let userDefinedAddress = self.userDefinedAddress, !userDefinedAddress.isEmpty else {
|
||||||
|
let fullAddress = MXTools.fullLocalAlias(from: defaultAddress, with: session)
|
||||||
|
|
||||||
|
if let publicAddress = self.publicAddress, !publicAddress.isEmpty {
|
||||||
|
addressValidationSubject.send(.current(fullAddress))
|
||||||
|
} else if defaultAddress.isEmpty {
|
||||||
|
addressValidationSubject.send(.none(fullAddress))
|
||||||
|
} else {
|
||||||
|
validate(defaultAddress)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(userDefinedAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func validate(_ aliasLocalPart: String) {
|
||||||
|
let fullAddress = MXTools.fullLocalAlias(from: aliasLocalPart, with: session)
|
||||||
|
|
||||||
|
if let publicAddress = self.publicAddress, publicAddress == aliasLocalPart {
|
||||||
|
self.addressValidationSubject.send(.current(fullAddress))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addressValidationOperation = MXRoomAliasAvailabilityChecker.validate(aliasLocalPart: aliasLocalPart, with: session) { [weak self] result in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self.addressValidationOperation = nil
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .available:
|
||||||
|
self.addressValidationSubject.send(.valid(fullAddress))
|
||||||
|
case .invalid:
|
||||||
|
self.addressValidationSubject.send(.invalidCharacters(fullAddress))
|
||||||
|
case .notAvailable:
|
||||||
|
self.addressValidationSubject.send(.alreadyExists(fullAddress))
|
||||||
|
case .serverError:
|
||||||
|
self.addressValidationSubject.send(.none(fullAddress))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateRoomProperties() {
|
||||||
|
guard let roomState = roomState else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let canonicalAlias = roomState.canonicalAlias {
|
||||||
|
let localAliasPart = MXTools.extractLocalAliasPart(from: canonicalAlias)
|
||||||
|
self.publicAddress = localAliasPart
|
||||||
|
self.defaultAddress = localAliasPart
|
||||||
|
} else {
|
||||||
|
self.publicAddress = nil
|
||||||
|
self.defaultAddress = MXTools.validAliasLocalPart(from: roomState.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.roomProperties = SpaceSettingsRoomProperties(
|
||||||
|
name: roomState.name,
|
||||||
|
topic: roomState.topic,
|
||||||
|
address: self.defaultAddress,
|
||||||
|
avatarUrl: roomState.avatar,
|
||||||
|
visibility: visibility(with: roomState),
|
||||||
|
allowedParentIds: allowedParentIds(with: roomState),
|
||||||
|
isAvatarEditable: isField(ofType: kMXEventTypeStringRoomAvatar, editableWith: roomState.powerLevels),
|
||||||
|
isNameEditable: isField(ofType: kMXEventTypeStringRoomName, editableWith: roomState.powerLevels),
|
||||||
|
isTopicEditable: isField(ofType: kMXEventTypeStringRoomTopic, editableWith: roomState.powerLevels),
|
||||||
|
isAddressEditable: isField(ofType: kMXEventTypeStringRoomAliases, editableWith: roomState.powerLevels),
|
||||||
|
isAccessEditable: isField(ofType: kMXEventTypeStringRoomJoinRules, editableWith: roomState.powerLevels))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Post process
|
||||||
|
|
||||||
|
private var currentTaskIndex: Int = 0
|
||||||
|
private var tasks: [PostProcessTask] = []
|
||||||
|
private var lastError: Error?
|
||||||
|
private var completion: ((_ result: SpaceSettingsServiceCompletionResult) -> Void)?
|
||||||
|
|
||||||
|
private enum PostProcessTaskType: Equatable {
|
||||||
|
case updateName(String)
|
||||||
|
case updateTopic(String)
|
||||||
|
case updateAlias(String)
|
||||||
|
case uploadAvatar(UIImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum PostProcessTaskState: CaseIterable, Equatable {
|
||||||
|
case none
|
||||||
|
case started
|
||||||
|
case success
|
||||||
|
case failure
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PostProcessTask: Equatable {
|
||||||
|
let type: PostProcessTaskType
|
||||||
|
var state: PostProcessTaskState = .none
|
||||||
|
var isFinished: Bool {
|
||||||
|
return state == .failure || state == .success
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: PostProcessTask, rhs: PostProcessTask) -> Bool {
|
||||||
|
return lhs.type == rhs.type && lhs.state == rhs.state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(roomName: String, topic: String, address: String, avatar: UIImage?,
|
||||||
|
completion: ((_ result: SpaceSettingsServiceCompletionResult) -> Void)?) {
|
||||||
|
// First attempt
|
||||||
|
if self.tasks.isEmpty {
|
||||||
|
var tasks: [PostProcessTask] = []
|
||||||
|
if roomProperties?.name ?? "" != roomName {
|
||||||
|
tasks.append(PostProcessTask(type: .updateName(roomName)))
|
||||||
|
}
|
||||||
|
if roomProperties?.topic ?? "" != topic {
|
||||||
|
tasks.append(PostProcessTask(type: .updateTopic(topic)))
|
||||||
|
}
|
||||||
|
if roomProperties?.address ?? "" != address {
|
||||||
|
tasks.append(PostProcessTask(type: .updateAlias(address)))
|
||||||
|
}
|
||||||
|
if let avatarImage = avatar {
|
||||||
|
tasks.append(PostProcessTask(type: .uploadAvatar(avatarImage)))
|
||||||
|
}
|
||||||
|
self.tasks = tasks
|
||||||
|
} else {
|
||||||
|
// Retry -> restart failed tasks
|
||||||
|
self.tasks = tasks.map({ task in
|
||||||
|
if task.state == .failure {
|
||||||
|
return PostProcessTask(type: task.type, state: .none)
|
||||||
|
}
|
||||||
|
return task
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isLoadingSubject.send(true)
|
||||||
|
self.completion = completion
|
||||||
|
self.lastError = nil
|
||||||
|
currentTaskIndex = -1
|
||||||
|
runNextTask()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func runNextTask() {
|
||||||
|
currentTaskIndex += 1
|
||||||
|
guard currentTaskIndex < tasks.count else {
|
||||||
|
self.isLoadingSubject.send(false)
|
||||||
|
if let error = lastError {
|
||||||
|
showPostProcessAlert.send(true)
|
||||||
|
completion?(.failure(error))
|
||||||
|
} else {
|
||||||
|
completion?(.success)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let task = tasks[currentTaskIndex]
|
||||||
|
|
||||||
|
guard !task.isFinished else {
|
||||||
|
runNextTask()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch task.type {
|
||||||
|
case .updateName(let roomName):
|
||||||
|
update(roomName: roomName)
|
||||||
|
case .updateTopic(let topic):
|
||||||
|
update(topic: topic)
|
||||||
|
case .updateAlias(let address):
|
||||||
|
update(canonicalAlias: address)
|
||||||
|
case .uploadAvatar(let image):
|
||||||
|
upload(avatar: image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateCurrentTaskState(with state: PostProcessTaskState) {
|
||||||
|
guard currentTaskIndex < tasks.count else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks[currentTaskIndex].state = state
|
||||||
|
}
|
||||||
|
|
||||||
|
private func update(roomName: String) {
|
||||||
|
updateCurrentTaskState(with: .started)
|
||||||
|
|
||||||
|
currentOperation = room?.setName(roomName, completion: { [weak self] response in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
switch response {
|
||||||
|
case .success:
|
||||||
|
self.updateCurrentTaskState(with: .success)
|
||||||
|
case .failure(let error):
|
||||||
|
self.lastError = error
|
||||||
|
self.updateCurrentTaskState(with: .failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.runNextTask()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private func update(topic: String) {
|
||||||
|
updateCurrentTaskState(with: .started)
|
||||||
|
|
||||||
|
currentOperation = room?.setTopic(topic, completion: { [weak self] response in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
switch response {
|
||||||
|
case .success:
|
||||||
|
self.updateCurrentTaskState(with: .success)
|
||||||
|
case .failure(let error):
|
||||||
|
self.lastError = error
|
||||||
|
self.updateCurrentTaskState(with: .failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.runNextTask()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private func update(canonicalAlias: String) {
|
||||||
|
updateCurrentTaskState(with: .started)
|
||||||
|
|
||||||
|
currentOperation = room?.addAlias(MXTools.fullLocalAlias(from: canonicalAlias, with: session), completion: { [weak self] response in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
switch response {
|
||||||
|
case .success:
|
||||||
|
if let publicAddress = self.publicAddress {
|
||||||
|
self.currentOperation = self.room?.removeAlias(MXTools.fullLocalAlias(from: publicAddress, with: self.session), completion: { [weak self] response in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self.setup(canonicalAlias: canonicalAlias)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
self.setup(canonicalAlias: canonicalAlias)
|
||||||
|
}
|
||||||
|
case .failure(let error):
|
||||||
|
self.lastError = error
|
||||||
|
self.updateCurrentTaskState(with: .failure)
|
||||||
|
self.runNextTask()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setup(canonicalAlias: String) {
|
||||||
|
currentOperation = room?.setCanonicalAlias(MXTools.fullLocalAlias(from: canonicalAlias, with: session), completion: { [weak self] response in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
switch response {
|
||||||
|
case .success:
|
||||||
|
self.updateCurrentTaskState(with: .success)
|
||||||
|
case .failure(let error):
|
||||||
|
self.lastError = error
|
||||||
|
self.updateCurrentTaskState(with: .failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.runNextTask()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private func upload(avatar: UIImage) {
|
||||||
|
updateCurrentTaskState(with: .started)
|
||||||
|
|
||||||
|
let avatarUp = MXKTools.forceImageOrientationUp(avatar)
|
||||||
|
|
||||||
|
mediaUploader.uploadData(avatarUp?.jpegData(compressionQuality: 0.5), filename: nil, mimeType: "image/jpeg",
|
||||||
|
success: { [weak self] (urlString) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
guard let urlString = urlString else {
|
||||||
|
self.updateCurrentTaskState(with: .failure)
|
||||||
|
self.runNextTask()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let url = URL(string: urlString) else {
|
||||||
|
self.updateCurrentTaskState(with: .failure)
|
||||||
|
self.runNextTask()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.setAvatar(withURL: url)
|
||||||
|
},
|
||||||
|
failure: { [weak self] (error) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self.lastError = error
|
||||||
|
self.updateCurrentTaskState(with: .failure)
|
||||||
|
self.runNextTask()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setAvatar(withURL url: URL) {
|
||||||
|
currentOperation = room?.setAvatar(url: url) { [weak self] (response) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
switch response {
|
||||||
|
case .success:
|
||||||
|
self.updateCurrentTaskState(with: .success)
|
||||||
|
case .failure(let error):
|
||||||
|
self.lastError = error
|
||||||
|
self.updateCurrentTaskState(with: .failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.runNextTask()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
class MockSpaceSettingsService: SpaceSettingsServiceProtocol {
|
||||||
|
|
||||||
|
var spaceId: String
|
||||||
|
var roomProperties: SpaceSettingsRoomProperties?
|
||||||
|
private(set) var displayName: String?
|
||||||
|
|
||||||
|
var roomPropertiesSubject: CurrentValueSubject<SpaceSettingsRoomProperties?, Never>
|
||||||
|
private(set) var isLoadingSubject: CurrentValueSubject<Bool, Never>
|
||||||
|
private(set) var showPostProcessAlert: CurrentValueSubject<Bool, Never>
|
||||||
|
private(set) var addressValidationSubject: CurrentValueSubject<SpaceCreationSettingsAddressValidationStatus, Never>
|
||||||
|
|
||||||
|
init(spaceId: String = "!\(UUID().uuidString):matrix.org",
|
||||||
|
roomProperties: SpaceSettingsRoomProperties? = nil,
|
||||||
|
displayName: String? = nil,
|
||||||
|
isLoading: Bool = false,
|
||||||
|
showPostProcessAlert: Bool = false) {
|
||||||
|
self.spaceId = spaceId
|
||||||
|
self.roomProperties = roomProperties
|
||||||
|
self.displayName = displayName
|
||||||
|
self.isLoadingSubject = CurrentValueSubject(isLoading)
|
||||||
|
self.showPostProcessAlert = CurrentValueSubject(showPostProcessAlert)
|
||||||
|
self.roomPropertiesSubject = CurrentValueSubject(roomProperties)
|
||||||
|
self.addressValidationSubject = CurrentValueSubject(.none(spaceId))
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(roomName: String, topic: String, address: String, avatar: UIImage?, completion: ((SpaceSettingsServiceCompletionResult) -> Void)?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func addressDidChange(_ newValue: String) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func simulateUpdate(addressValidationStatus: SpaceCreationSettingsAddressValidationStatus) {
|
||||||
|
self.addressValidationSubject.value = addressValidationStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
enum SpaceSettingsServiceCompletionResult {
|
||||||
|
case success
|
||||||
|
case failure(Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
protocol SpaceSettingsServiceProtocol: Avatarable {
|
||||||
|
var spaceId: String { get }
|
||||||
|
var roomProperties: SpaceSettingsRoomProperties? { get }
|
||||||
|
|
||||||
|
var isLoadingSubject: CurrentValueSubject<Bool, Never> { get }
|
||||||
|
var roomPropertiesSubject: CurrentValueSubject<SpaceSettingsRoomProperties?, Never> { get }
|
||||||
|
var showPostProcessAlert: CurrentValueSubject<Bool, Never> { get }
|
||||||
|
var addressValidationSubject: CurrentValueSubject<SpaceCreationSettingsAddressValidationStatus, Never> { get }
|
||||||
|
|
||||||
|
func update(roomName: String, topic: String, address: String, avatar: UIImage?, completion: ((_ result: SpaceSettingsServiceCompletionResult) -> Void)?)
|
||||||
|
func addressDidChange(_ newValue: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Avatarable
|
||||||
|
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
extension SpaceSettingsServiceProtocol {
|
||||||
|
var mxContentUri: String? {
|
||||||
|
roomProperties?.avatarUrl
|
||||||
|
}
|
||||||
|
var matrixItemId: String {
|
||||||
|
spaceId
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// MARK: - Coordinator
|
||||||
|
|
||||||
|
enum SpaceSettingsCoordinatorResult {
|
||||||
|
case cancel
|
||||||
|
case done
|
||||||
|
case optionScreen(_ optionType: SpaceSettingsOptionType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: View model
|
||||||
|
|
||||||
|
enum SpaceSettingsViewModelResult {
|
||||||
|
case cancel
|
||||||
|
case done
|
||||||
|
case optionScreen(_ optionType: SpaceSettingsOptionType)
|
||||||
|
case pickImage(_ sourceRect: CGRect)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: View
|
||||||
|
|
||||||
|
enum SpaceSettingsVisibility: CaseIterable {
|
||||||
|
case `private`
|
||||||
|
case restricted
|
||||||
|
case `public`
|
||||||
|
|
||||||
|
var stringValue: String {
|
||||||
|
switch self {
|
||||||
|
case .private:
|
||||||
|
return VectorL10n.private
|
||||||
|
case .public:
|
||||||
|
return VectorL10n.public
|
||||||
|
case .restricted:
|
||||||
|
return VectorL10n.createRoomTypeRestricted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SpaceSettingsRoomProperties {
|
||||||
|
let name: String?
|
||||||
|
let topic: String?
|
||||||
|
let address: String?
|
||||||
|
let avatarUrl: String?
|
||||||
|
let visibility: SpaceSettingsVisibility
|
||||||
|
let allowedParentIds: [String]
|
||||||
|
let isAvatarEditable: Bool
|
||||||
|
let isNameEditable: Bool
|
||||||
|
let isTopicEditable: Bool
|
||||||
|
let isAddressEditable: Bool
|
||||||
|
let isAccessEditable: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SpaceSettingsViewState: BindableState {
|
||||||
|
let defaultAddress: String
|
||||||
|
let avatar: AvatarInputProtocol
|
||||||
|
var roomProperties: SpaceSettingsRoomProperties?
|
||||||
|
var userSelectedAvatar: UIImage?
|
||||||
|
var showRoomAddress: Bool
|
||||||
|
let roomNameError: String?
|
||||||
|
var addressMessage: String?
|
||||||
|
var isAddressValid: Bool
|
||||||
|
var isLoading: Bool
|
||||||
|
var visibilityString: String
|
||||||
|
var options: [SpaceSettingsOption]
|
||||||
|
var isModified: Bool {
|
||||||
|
userSelectedAvatar != nil || isRoomNameModified || isTopicModified || isAddressModified
|
||||||
|
}
|
||||||
|
var isRoomNameModified: Bool {
|
||||||
|
(roomProperties?.name ?? "") != bindings.name
|
||||||
|
}
|
||||||
|
var isTopicModified: Bool {
|
||||||
|
(roomProperties?.topic ?? "") != bindings.topic
|
||||||
|
}
|
||||||
|
var isAddressModified: Bool {
|
||||||
|
(roomProperties?.address ?? "") != bindings.address
|
||||||
|
}
|
||||||
|
var bindings: SpaceSettingsViewModelBindings
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SpaceSettingsViewModelBindings {
|
||||||
|
var name: String
|
||||||
|
var topic: String
|
||||||
|
var address: String
|
||||||
|
var showPostProcessAlert: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SpaceSettingsOption: Identifiable {
|
||||||
|
let id: SpaceSettingsOptionType
|
||||||
|
let icon: UIImage?
|
||||||
|
let title: String?
|
||||||
|
let value: String?
|
||||||
|
let isEnabled: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SpaceSettingsOptionType {
|
||||||
|
case visibility
|
||||||
|
case rooms
|
||||||
|
case members
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SpaceSettingsViewAction {
|
||||||
|
case done(_ name: String, _ topic: String, _ address: String, _ userSelectedAvatar: UIImage?)
|
||||||
|
case cancel
|
||||||
|
case pickImage(_ sourceRect: CGRect)
|
||||||
|
case optionSelected(_ optionType: SpaceSettingsOptionType)
|
||||||
|
case addressChanged(_ newValue: String)
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
//
|
||||||
|
// 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 SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
@available(iOS 14, *)
|
||||||
|
typealias SpaceSettingsViewModelType = StateStoreViewModel<SpaceSettingsViewState,
|
||||||
|
Never,
|
||||||
|
SpaceSettingsViewAction>
|
||||||
|
@available(iOS 14, *)
|
||||||
|
class SpaceSettingsViewModel: SpaceSettingsViewModelType, SpaceSettingsViewModelProtocol {
|
||||||
|
|
||||||
|
// MARK: - Properties
|
||||||
|
|
||||||
|
private static let options: [SpaceSettingsOption] = [
|
||||||
|
SpaceSettingsOption(id: .rooms, icon: Asset.Images.spaceRoomIcon.image, title: VectorL10n.titleRooms, value: nil, isEnabled: true),
|
||||||
|
SpaceSettingsOption(id: .members, icon: Asset.Images.spaceMenuMembers.image, title: VectorL10n.roomDetailsPeople, value: nil, isEnabled: true)
|
||||||
|
]
|
||||||
|
|
||||||
|
// MARK: Private
|
||||||
|
|
||||||
|
private let service: SpaceSettingsServiceProtocol
|
||||||
|
|
||||||
|
// MARK: Public
|
||||||
|
|
||||||
|
var completion: ((SpaceSettingsViewModelResult) -> Void)?
|
||||||
|
|
||||||
|
// MARK: - Setup
|
||||||
|
|
||||||
|
static func makeSpaceSettingsViewModel(service: SpaceSettingsServiceProtocol) -> SpaceSettingsViewModelProtocol {
|
||||||
|
return SpaceSettingsViewModel(service: service)
|
||||||
|
}
|
||||||
|
|
||||||
|
private init(service: SpaceSettingsServiceProtocol) {
|
||||||
|
self.service = service
|
||||||
|
super.init(initialViewState: Self.defaultState(with: service, validationStatus: service.addressValidationSubject.value))
|
||||||
|
setupObservers()
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func defaultState(with service: SpaceSettingsServiceProtocol, validationStatus: SpaceCreationSettingsAddressValidationStatus) -> SpaceSettingsViewState {
|
||||||
|
let bindings = SpaceSettingsViewModelBindings(
|
||||||
|
name: service.roomProperties?.name ?? "",
|
||||||
|
topic: service.roomProperties?.topic ?? "",
|
||||||
|
address: service.roomProperties?.address ?? "",
|
||||||
|
showPostProcessAlert: service.showPostProcessAlert.value)
|
||||||
|
|
||||||
|
return SpaceSettingsViewState(
|
||||||
|
defaultAddress: service.roomProperties?.address ?? "",
|
||||||
|
avatar: AvatarInput(mxContentUri: service.mxContentUri, matrixItemId: service.matrixItemId, displayName: service.displayName),
|
||||||
|
roomProperties: service.roomProperties,
|
||||||
|
userSelectedAvatar: nil,
|
||||||
|
showRoomAddress: service.roomProperties?.visibility == .public,
|
||||||
|
roomNameError: nil,
|
||||||
|
addressMessage: validationStatus.message,
|
||||||
|
isAddressValid: validationStatus.isValid,
|
||||||
|
isLoading: service.isLoadingSubject.value,
|
||||||
|
visibilityString: (service.roomProperties?.visibility ?? .private).stringValue,
|
||||||
|
options: options,
|
||||||
|
bindings: bindings)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupObservers() {
|
||||||
|
service.isLoadingSubject.sink { [weak self] isLoading in
|
||||||
|
self?.state.isLoading = isLoading
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
service.showPostProcessAlert.sink { [weak self] showPostProcessAlert in
|
||||||
|
self?.state.bindings.showPostProcessAlert = showPostProcessAlert
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
service.roomPropertiesSubject.sink { [weak self] roomProperties in
|
||||||
|
guard let roomProperties = roomProperties, let self = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.propertiesUpdated(roomProperties)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
service.addressValidationSubject.sink { [weak self] validationStatus in
|
||||||
|
self?.state.addressMessage = validationStatus.message
|
||||||
|
self?.state.isAddressValid = validationStatus.isValid
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public
|
||||||
|
|
||||||
|
override func process(viewAction: SpaceSettingsViewAction) {
|
||||||
|
switch viewAction {
|
||||||
|
case .cancel:
|
||||||
|
cancel()
|
||||||
|
case .pickImage(let sourceRect):
|
||||||
|
completion?(.pickImage(sourceRect))
|
||||||
|
case .optionSelected(let optionType):
|
||||||
|
completion?(.optionScreen(optionType))
|
||||||
|
case .done(let name, let topic, let address, let userSelectedAvatar):
|
||||||
|
service.update(roomName: name, topic: topic, address: address, avatar: userSelectedAvatar) { [weak self] result in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
self.done()
|
||||||
|
case .failure:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .addressChanged(let newValue):
|
||||||
|
service.addressDidChange(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAvatarImage(with image: UIImage?) {
|
||||||
|
state.userSelectedAvatar = image
|
||||||
|
}
|
||||||
|
|
||||||
|
private func propertiesUpdated(_ roomProperties: SpaceSettingsRoomProperties) {
|
||||||
|
state.roomProperties = roomProperties
|
||||||
|
if !state.isRoomNameModified {
|
||||||
|
state.bindings.name = roomProperties.name ?? ""
|
||||||
|
}
|
||||||
|
if !state.isTopicModified {
|
||||||
|
state.bindings.topic = roomProperties.topic ?? ""
|
||||||
|
}
|
||||||
|
if !state.isAddressModified {
|
||||||
|
state.bindings.address = roomProperties.address ?? ""
|
||||||
|
}
|
||||||
|
state.visibilityString = roomProperties.visibility.stringValue
|
||||||
|
state.showRoomAddress = roomProperties.visibility == .public
|
||||||
|
}
|
||||||
|
|
||||||
|
private func done() {
|
||||||
|
completion?(.done)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cancel() {
|
||||||
|
completion?(.cancel)
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
protocol SpaceSettingsViewModelProtocol {
|
||||||
|
|
||||||
|
var completion: ((SpaceSettingsViewModelResult) -> Void)? { get set }
|
||||||
|
@available(iOS 14, *)
|
||||||
|
static func makeSpaceSettingsViewModel(service: SpaceSettingsServiceProtocol) -> SpaceSettingsViewModelProtocol
|
||||||
|
@available(iOS 14, *)
|
||||||
|
var context: SpaceSettingsViewModelType.Context { get }
|
||||||
|
func updateAvatarImage(with image: UIImage?)
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// 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 XCTest
|
||||||
|
import RiotSwiftUI
|
||||||
|
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
class SpaceSettingsUITests: MockScreenTest {
|
||||||
|
|
||||||
|
override class var screenType: MockScreenState.Type {
|
||||||
|
return MockSpaceSettingsScreenState.self
|
||||||
|
}
|
||||||
|
|
||||||
|
override class func createTest() -> MockScreenTest {
|
||||||
|
return SpaceSettingsUITests(selector: #selector(verifySpaceSettingsScreen))
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifySpaceSettingsScreen() throws {
|
||||||
|
guard let screenState = screenState as? MockSpaceSettingsScreenState else { fatalError("no screen") }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
//
|
||||||
|
// 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 XCTest
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
@testable import RiotSwiftUI
|
||||||
|
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
class SpaceSettingsViewModelTests: XCTestCase {
|
||||||
|
let creationParameters = SpaceCreationParameters()
|
||||||
|
var service: MockSpaceSettingsService!
|
||||||
|
var viewModel: SpaceSettingsViewModelProtocol!
|
||||||
|
var context: SpaceSettingsViewModelType.Context!
|
||||||
|
var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
let roomProperties = SpaceSettingsRoomProperties(
|
||||||
|
name: "Space Name",
|
||||||
|
topic: "Sapce topic",
|
||||||
|
address: "#fake:matrix.org",
|
||||||
|
avatarUrl: nil,
|
||||||
|
visibility: .public,
|
||||||
|
allowedParentIds: [],
|
||||||
|
isAvatarEditable: true,
|
||||||
|
isNameEditable: true,
|
||||||
|
isTopicEditable: true,
|
||||||
|
isAddressEditable: true,
|
||||||
|
isAccessEditable: true)
|
||||||
|
|
||||||
|
service = MockSpaceSettingsService(roomProperties: roomProperties, displayName: roomProperties.name, isLoading: false, showPostProcessAlert: false)
|
||||||
|
viewModel = SpaceSettingsViewModel.makeSpaceSettingsViewModel(service: service)
|
||||||
|
context = viewModel.context
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAddressAlready() throws {
|
||||||
|
service.simulateUpdate(addressValidationStatus: .alreadyExists("#fake:matrix.org"))
|
||||||
|
XCTAssertEqual(context.viewState.isAddressValid, false)
|
||||||
|
XCTAssertEqual(context.viewState.addressMessage, VectorL10n.spacesCreationAddressAlreadyExists("#fake:matrix.org"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInvalidAddress() throws {
|
||||||
|
service.simulateUpdate(addressValidationStatus: .invalidCharacters("#fake:matrix.org"))
|
||||||
|
XCTAssertEqual(context.viewState.isAddressValid, false)
|
||||||
|
XCTAssertEqual(context.viewState.addressMessage, VectorL10n.spacesCreationAddressInvalidCharacters("#fake:matrix.org"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
//
|
||||||
|
// 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 SwiftUI
|
||||||
|
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
struct SpaceSettings: View {
|
||||||
|
|
||||||
|
// MARK: - Properties
|
||||||
|
|
||||||
|
// MARK: Private
|
||||||
|
|
||||||
|
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||||
|
|
||||||
|
// MARK: Public
|
||||||
|
|
||||||
|
@ObservedObject var viewModel: SpaceSettingsViewModel.Context
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView {
|
||||||
|
VStack {
|
||||||
|
avatarView
|
||||||
|
Spacer().frame(height:32)
|
||||||
|
formView
|
||||||
|
roomAccess
|
||||||
|
options
|
||||||
|
.padding(.bottom, 32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(theme.colors.navigation)
|
||||||
|
.waitOverlay(show: viewModel.viewState.isLoading, allowUserInteraction: false)
|
||||||
|
.ignoresSafeArea(.container, edges: .bottom)
|
||||||
|
.frame(maxHeight: .infinity)
|
||||||
|
.navigationBarBackButtonHidden(true)
|
||||||
|
.navigationTitle(VectorL10n.settingsTitle)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
|
Button(VectorL10n.done) {
|
||||||
|
updateSpace()
|
||||||
|
}
|
||||||
|
.disabled(!viewModel.viewState.isModified || !viewModel.viewState.isAddressValid)
|
||||||
|
}
|
||||||
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
|
Button(VectorL10n.cancel) {
|
||||||
|
viewModel.send(viewAction: .cancel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.accentColor(theme.colors.accent)
|
||||||
|
.alert(isPresented: $viewModel.showPostProcessAlert, content: {
|
||||||
|
Alert(title: Text(VectorL10n.settingsTitle),
|
||||||
|
message: Text(VectorL10n.spaceSettingsUpdateFailedMessage),
|
||||||
|
primaryButton: .default(Text(VectorL10n.retry), action: {
|
||||||
|
updateSpace()
|
||||||
|
}),
|
||||||
|
secondaryButton: .cancel())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var avatarView: some View {
|
||||||
|
ZStack(alignment: .bottomTrailing) {
|
||||||
|
GeometryReader { reader in
|
||||||
|
ZStack {
|
||||||
|
SpaceAvatarImage(mxContentUri: viewModel.viewState.avatar.mxContentUri, matrixItemId: viewModel.viewState.avatar.matrixItemId, displayName: viewModel.viewState.avatar.displayName, size: .xxLarge)
|
||||||
|
.padding(6)
|
||||||
|
if let image = viewModel.viewState.userSelectedAvatar {
|
||||||
|
Image(uiImage: image)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 80, height: 80, alignment: .center)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
}.padding(10)
|
||||||
|
.onTapGesture {
|
||||||
|
guard viewModel.viewState.roomProperties?.isAvatarEditable == true else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ResponderManager.resignFirstResponder()
|
||||||
|
viewModel.send(viewAction: .pickImage(reader.frame(in: .global)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if viewModel.viewState.roomProperties?.isAvatarEditable == true {
|
||||||
|
Image(uiImage: Asset.Images.spaceCreationCamera.image)
|
||||||
|
.renderingMode(.template)
|
||||||
|
.foregroundColor(theme.colors.secondaryContent)
|
||||||
|
.frame(width: 32, height: 32, alignment: .center)
|
||||||
|
.background(theme.colors.background)
|
||||||
|
.clipShape(Circle())
|
||||||
|
}
|
||||||
|
}.frame(width: 104, height: 104)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var formView: some View {
|
||||||
|
VStack{
|
||||||
|
RoundedBorderTextField(
|
||||||
|
title: VectorL10n.createRoomPlaceholderName,
|
||||||
|
placeHolder: "",
|
||||||
|
text: $viewModel.name,
|
||||||
|
footerText: .constant(viewModel.viewState.roomNameError),
|
||||||
|
isError: .constant(true),
|
||||||
|
configuration: UIKitTextInputConfiguration( returnKeyType: .next))
|
||||||
|
.padding(.horizontal, 2)
|
||||||
|
.padding(.bottom, 20)
|
||||||
|
.disabled(viewModel.viewState.roomProperties?.isNameEditable != true)
|
||||||
|
RoundedBorderTextEditor(
|
||||||
|
title: VectorL10n.spaceTopic,
|
||||||
|
placeHolder: VectorL10n.spaceTopic,
|
||||||
|
text: $viewModel.topic,
|
||||||
|
textMaxHeight: 72,
|
||||||
|
error: .constant(nil))
|
||||||
|
.padding(.horizontal, 2)
|
||||||
|
.padding(.bottom, viewModel.viewState.showRoomAddress ? 20 : 3)
|
||||||
|
.disabled(viewModel.viewState.roomProperties?.isTopicEditable != true)
|
||||||
|
if viewModel.viewState.showRoomAddress {
|
||||||
|
RoundedBorderTextField(
|
||||||
|
title: VectorL10n.spacesCreationAddress,
|
||||||
|
placeHolder: "# \(viewModel.viewState.defaultAddress)",
|
||||||
|
text: $viewModel.address,
|
||||||
|
footerText: .constant(viewModel.viewState.addressMessage),
|
||||||
|
isError: .constant(!viewModel.viewState.isAddressValid),
|
||||||
|
configuration: UIKitTextInputConfiguration(keyboardType: .URL, returnKeyType: .done, autocapitalizationType: .none), onTextChanged: {
|
||||||
|
newText in
|
||||||
|
viewModel.send(viewAction: .addressChanged(newText))
|
||||||
|
})
|
||||||
|
.disabled(viewModel.viewState.roomProperties?.isAddressEditable != true)
|
||||||
|
.padding(.horizontal, 2)
|
||||||
|
.padding(.bottom, 3)
|
||||||
|
.accessibility(identifier: "addressTextField")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var roomAccess: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Spacer().frame(height:24)
|
||||||
|
Text(VectorL10n.spaceSettingsAccessSection)
|
||||||
|
.font(theme.fonts.footnote)
|
||||||
|
.foregroundColor(theme.colors.secondaryContent)
|
||||||
|
.padding(.leading)
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
SpaceSettingsOptionListItem(
|
||||||
|
title: VectorL10n.roomDetailsAccessRowTitle,
|
||||||
|
value: viewModel.viewState.visibilityString) {
|
||||||
|
ResponderManager.resignFirstResponder()
|
||||||
|
viewModel.send(viewAction: .optionSelected(.visibility))
|
||||||
|
}
|
||||||
|
.disabled(viewModel.viewState.roomProperties?.isAccessEditable != true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var options: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 1) {
|
||||||
|
Spacer().frame(height: 50)
|
||||||
|
Text(VectorL10n.settingsTitle.uppercased())
|
||||||
|
.font(theme.fonts.footnote)
|
||||||
|
.foregroundColor(theme.colors.secondaryContent)
|
||||||
|
.padding(.leading)
|
||||||
|
.padding(.bottom, 8)
|
||||||
|
ForEach(viewModel.viewState.options) { option in
|
||||||
|
SpaceSettingsOptionListItem(
|
||||||
|
icon: option.icon,
|
||||||
|
title: option.title,
|
||||||
|
value: option.value) {
|
||||||
|
ResponderManager.resignFirstResponder()
|
||||||
|
viewModel.send(viewAction: .optionSelected(option.id))
|
||||||
|
}
|
||||||
|
.disabled(!option.isEnabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateSpace() {
|
||||||
|
viewModel.send(viewAction: .done(viewModel.name, viewModel.topic, viewModel.address, viewModel.viewState.userSelectedAvatar))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Previews
|
||||||
|
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
struct SpaceSettings_Previews: PreviewProvider {
|
||||||
|
static let stateRenderer = MockSpaceSettingsScreenState.stateRenderer
|
||||||
|
static var previews: some View {
|
||||||
|
stateRenderer.screenGroup()
|
||||||
|
stateRenderer.screenGroup().theme(.dark).preferredColorScheme(.dark)
|
||||||
|
}
|
||||||
|
}
|
||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
//
|
||||||
|
// 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 SwiftUI
|
||||||
|
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
struct SpaceSettingsOptionListItem: View {
|
||||||
|
|
||||||
|
// MARK: Private
|
||||||
|
|
||||||
|
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||||
|
@Environment(\.isEnabled) private var isEnabled
|
||||||
|
|
||||||
|
// MARK: - Properties
|
||||||
|
|
||||||
|
let icon: UIImage?
|
||||||
|
let title: String?
|
||||||
|
let value: String?
|
||||||
|
let action: (() -> Void)?
|
||||||
|
|
||||||
|
// MARK: - Setup
|
||||||
|
|
||||||
|
init(icon: UIImage? = nil,
|
||||||
|
title: String? = nil,
|
||||||
|
value: String? = nil,
|
||||||
|
action: (() -> Void)? = nil) {
|
||||||
|
self.icon = icon
|
||||||
|
self.title = title
|
||||||
|
self.value = value
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
HStack(alignment: .center, spacing: 16) {
|
||||||
|
if let icon = icon {
|
||||||
|
Image(uiImage: icon)
|
||||||
|
.renderingMode(.template)
|
||||||
|
.frame(width: 22, height: 22)
|
||||||
|
.foregroundColor(theme.colors.tertiaryContent)
|
||||||
|
}
|
||||||
|
if let title = title {
|
||||||
|
Text(title)
|
||||||
|
.font(theme.fonts.body)
|
||||||
|
.foregroundColor(theme.colors.primaryContent)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
if let value = value {
|
||||||
|
Text(value)
|
||||||
|
.font(theme.fonts.body)
|
||||||
|
.foregroundColor(theme.colors.tertiaryContent)
|
||||||
|
}
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
.renderingMode(.template)
|
||||||
|
.font(.system(size: 16, weight: .regular))
|
||||||
|
.foregroundColor(theme.colors.quarterlyContent)
|
||||||
|
}
|
||||||
|
.opacity(isEnabled ? 1 : 0.5)
|
||||||
|
}
|
||||||
|
.frame(height: 44)
|
||||||
|
.padding(.horizontal)
|
||||||
|
.background(theme.colors.background)
|
||||||
|
.onTapGesture {
|
||||||
|
if isEnabled {
|
||||||
|
action?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Previews
|
||||||
|
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
struct SpaceSettingsOptionListItem_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
sampleView.theme(.light).preferredColorScheme(.light)
|
||||||
|
sampleView.theme(.dark).preferredColorScheme(.dark)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var sampleView: some View {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
SpaceSettingsOptionListItem(icon: nil, title: "Some Title", value: nil)
|
||||||
|
SpaceSettingsOptionListItem(icon: nil, title: "Some Title", value: "Some value")
|
||||||
|
SpaceSettingsOptionListItem(icon: Asset.Images.spaceRoomIcon.image, title: "Some Title", value: "Some value")
|
||||||
|
SpaceSettingsOptionListItem(icon: Asset.Images.spaceRoomIcon.image, title: "Some Title", value: "Some value")
|
||||||
|
.disabled(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Space Settings
|
||||||
Reference in New Issue
Block a user