mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 15:22:39 +00:00
SP4: space settings (#5730)
* SP4: Space Settings - Space settings screen implemented - No space upgrade available as per Element web - Need more insights for the space address field - Added settings live update - Added local alias implementation
This commit is contained in:
parent
8be08afd2d
commit
1fcf96865c
43 changed files with 2184 additions and 118 deletions
|
@ -1886,7 +1886,11 @@ Tap the + to start adding people.";
|
|||
"spaces_add_space" = "Add space";
|
||||
"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
|
||||
|
||||
|
|
|
@ -5543,11 +5543,23 @@ public class VectorL10n: NSObject {
|
|||
public static var spacePublicJoinRuleDetail: String {
|
||||
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
|
||||
public static var spaceTag: String {
|
||||
return VectorL10n.tr("Vector", "space_tag")
|
||||
}
|
||||
/// description
|
||||
/// Description
|
||||
public static var spaceTopic: String {
|
||||
return VectorL10n.tr("Vector", "space_topic")
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
|
|||
private let room: MXRoom
|
||||
private let parentSpaceId: String?
|
||||
private let initialSection: RoomInfoSection
|
||||
private let dismissOnCancel: Bool
|
||||
private weak var roomSettingsViewController: RoomSettingsViewController?
|
||||
|
||||
private lazy var segmentedViewController: SegmentedViewController = {
|
||||
|
@ -103,6 +104,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
|
|||
self.room = parameters.room
|
||||
self.parentSpaceId = parameters.parentSpaceId
|
||||
self.initialSection = parameters.initialSection
|
||||
self.dismissOnCancel = parameters.dismissOnCancel
|
||||
}
|
||||
|
||||
// MARK: - Public methods
|
||||
|
@ -226,6 +228,22 @@ extension RoomInfoCoordinator: RoomNotificationSettingsCoordinatorDelegate {
|
|||
}
|
||||
|
||||
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!) {
|
||||
self.delegate?.roomInfoCoordinator(self, didReplaceRoomWithReplacementId: newRoomId)
|
||||
}
|
||||
|
|
|
@ -32,16 +32,22 @@ class RoomInfoCoordinatorParameters: NSObject {
|
|||
let room: MXRoom
|
||||
let parentSpaceId: String?
|
||||
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.room = room
|
||||
self.parentSpaceId = parentSpaceId
|
||||
self.initialSection = initialSection
|
||||
self.dismissOnCancel = dismissOnCancel
|
||||
super.init()
|
||||
}
|
||||
|
||||
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)roomSettingsViewControllerDidCancel:(RoomSettingsViewController *)controller;
|
||||
|
||||
- (void)roomSettingsViewControllerDidComplete:(RoomSettingsViewController *)controller;
|
||||
|
||||
@end
|
||||
|
|
|
@ -906,7 +906,14 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
|
|||
|
||||
[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
|
||||
{
|
||||
[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 withdrawViewControllerAnimated:YES completion:nil];
|
||||
if (self.delegate)
|
||||
{
|
||||
[self.delegate roomSettingsViewControllerDidComplete:self];
|
||||
}
|
||||
else
|
||||
{
|
||||
[self withdrawViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UITableViewDataSource
|
||||
|
|
|
@ -66,6 +66,7 @@ final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType {
|
|||
private var membersCoordinator: SpaceMembersCoordinator?
|
||||
private var createSpaceCoordinator: SpaceCreationCoordinator?
|
||||
private var createRoomCoordinator: CreateRoomCoordinator?
|
||||
private var spaceSettingsCoordinator: Coordinator?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
|
@ -297,10 +298,27 @@ final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType {
|
|||
presentable.presentationController?.delegate = self
|
||||
toPresentable().present(presentable, animated: true, completion: nil)
|
||||
createRoomCoordinator.start()
|
||||
self.add(childCoordinator: 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
|
||||
|
||||
private func registerUserSessionsServiceNotifications() {
|
||||
|
@ -405,7 +423,11 @@ extension SideMenuCoordinator: SpaceMenuPresenterDelegate {
|
|||
case .addSpace:
|
||||
AppDelegate.theDelegate().showAlert(withTitle: VectorL10n.spacesAddSpace, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName))
|
||||
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 {
|
||||
func createRoomCoordinator(_ coordinator: CreateRoomCoordinatorType, didCreateNewRoom room: MXRoom) {
|
||||
coordinator.toPresentable().dismiss(animated: true) {
|
||||
self.remove(childCoordinator: coordinator)
|
||||
self.createRoomCoordinator = nil
|
||||
self.parameters.appNavigator.sideMenu.dismiss(animated: true) {
|
||||
|
||||
|
@ -461,7 +482,6 @@ extension SideMenuCoordinator: CreateRoomCoordinatorDelegate {
|
|||
|
||||
func createRoomCoordinator(_ coordinator: CreateRoomCoordinatorType, didAddRoomsWithIds roomIds: [String]) {
|
||||
coordinator.toPresentable().dismiss(animated: true) {
|
||||
self.remove(childCoordinator: coordinator)
|
||||
self.createRoomCoordinator = nil
|
||||
self.parameters.appNavigator.sideMenu.dismiss(animated: true) {
|
||||
|
||||
|
@ -474,7 +494,6 @@ extension SideMenuCoordinator: CreateRoomCoordinatorDelegate {
|
|||
|
||||
func createRoomCoordinatorDidCancel(_ coordinator: CreateRoomCoordinatorType) {
|
||||
coordinator.toPresentable().dismiss(animated: true) {
|
||||
self.remove(childCoordinator: coordinator)
|
||||
self.createRoomCoordinator = nil
|
||||
}
|
||||
}
|
||||
|
@ -488,5 +507,6 @@ extension SideMenuCoordinator: UIAdaptivePresentationControllerDelegate {
|
|||
self.membersCoordinator = nil
|
||||
self.createSpaceCoordinator = nil
|
||||
self.createRoomCoordinator = nil
|
||||
self.spaceSettingsCoordinator = nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ struct SpaceMemberDetailCoordinatorParameters {
|
|||
let member: MXRoomMember
|
||||
let session: MXSession
|
||||
let spaceId: String
|
||||
let showCancelMenuItem: Bool
|
||||
}
|
||||
|
||||
final class SpaceMemberDetailCoordinator: NSObject, SpaceMemberDetailCoordinatorType {
|
||||
|
@ -49,7 +50,7 @@ final class SpaceMemberDetailCoordinator: NSObject, SpaceMemberDetailCoordinator
|
|||
init(parameters: SpaceMemberDetailCoordinatorParameters) {
|
||||
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)
|
||||
spaceMemberDetailViewController.enableMention = true
|
||||
spaceMemberDetailViewController.enableVoipCall = false
|
||||
|
|
|
@ -106,11 +106,13 @@ final class SpaceMemberDetailViewController: RoomMemberDetailsViewController {
|
|||
}
|
||||
|
||||
private func setupViews() {
|
||||
let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in
|
||||
self?.cancelButtonAction()
|
||||
if viewModel.showCancelMenuItem {
|
||||
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) {
|
||||
|
|
|
@ -29,6 +29,7 @@ final class SpaceMemberDetailViewModel: NSObject, SpaceMemberDetailViewModelType
|
|||
private let member: MXRoomMember
|
||||
private let spaceId: String
|
||||
private var space: MXSpace?
|
||||
private(set) var showCancelMenuItem: Bool
|
||||
|
||||
private var currentOperation: MXHTTPOperation?
|
||||
|
||||
|
@ -39,11 +40,12 @@ final class SpaceMemberDetailViewModel: NSObject, SpaceMemberDetailViewModelType
|
|||
|
||||
// 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.session = session
|
||||
self.member = member
|
||||
self.spaceId = spaceId
|
||||
self.showCancelMenuItem = showCancelMenuItem
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
|
|
@ -32,6 +32,7 @@ protocol SpaceMemberDetailViewModelType {
|
|||
|
||||
var viewDelegate: SpaceMemberDetailViewModelViewDelegate? { get set }
|
||||
var coordinatorDelegate: SpaceMemberDetailViewModelCoordinatorDelegate? { get set }
|
||||
var showCancelMenuItem: Bool { get }
|
||||
|
||||
func process(viewAction: SpaceMemberDetailViewAction)
|
||||
}
|
||||
|
|
|
@ -110,7 +110,7 @@ final class SpaceMemberListViewController: RoomParticipantsViewController {
|
|||
self?.cancelButtonAction()
|
||||
}
|
||||
|
||||
self.navigationItem.rightBarButtonItem = cancelBarButtonItem
|
||||
self.navigationItem.leftBarButtonItem = cancelBarButtonItem
|
||||
|
||||
self.titleView = MainTitleView()
|
||||
self.titleView.titleLabel.text = VectorL10n.roomDetailsPeople
|
||||
|
|
|
@ -22,6 +22,17 @@ struct SpaceMembersCoordinatorParameters {
|
|||
let userSessionsService: UserSessionsService
|
||||
let session: MXSession
|
||||
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
|
||||
|
@ -46,7 +57,7 @@ final class SpaceMembersCoordinator: SpaceMembersCoordinatorType {
|
|||
|
||||
init(parameters: SpaceMembersCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
self.navigationRouter = NavigationRouter(navigationController: RiotNavigationController())
|
||||
self.navigationRouter = parameters.navigationRouter
|
||||
}
|
||||
|
||||
// MARK: - Public methods
|
||||
|
@ -59,8 +70,15 @@ final class SpaceMembersCoordinator: SpaceMembersCoordinatorType {
|
|||
|
||||
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 {
|
||||
return self.navigationRouter.toPresentable()
|
||||
|
@ -99,7 +117,7 @@ final class SpaceMembersCoordinator: SpaceMembersCoordinatorType {
|
|||
}
|
||||
|
||||
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)
|
||||
coordinator.delegate = self
|
||||
return coordinator
|
||||
|
@ -158,6 +176,7 @@ extension SpaceMembersCoordinator: SpaceMemberListCoordinatorDelegate {
|
|||
coordinator.delegate = self
|
||||
coordinator.start()
|
||||
self.childCoordinators.append(coordinator)
|
||||
self.navigationRouter.present(coordinator.toPresentable(), animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,8 +47,9 @@ final class ExploreRoomCoordinator: NSObject, ExploreRoomCoordinatorType {
|
|||
|
||||
// MARK: - Setup
|
||||
|
||||
init(session: MXSession, spaceId: String) {
|
||||
self.navigationRouter = NavigationRouter(navigationController: RiotNavigationController())
|
||||
init(session: MXSession, spaceId: String,
|
||||
navigationRouter: NavigationRouterType = NavigationRouter(navigationController: RiotNavigationController())) {
|
||||
self.navigationRouter = navigationRouter
|
||||
self.session = session
|
||||
self.spaceId = spaceId
|
||||
self.spaceIdStack = [spaceId]
|
||||
|
@ -65,7 +66,13 @@ final class ExploreRoomCoordinator: NSObject, ExploreRoomCoordinatorType {
|
|||
self.add(childCoordinator: 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 {
|
||||
|
@ -75,13 +82,17 @@ final class ExploreRoomCoordinator: NSObject, ExploreRoomCoordinatorType {
|
|||
// MARK: - Private methods
|
||||
|
||||
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()
|
||||
|
||||
self.add(childCoordinator: coordinator)
|
||||
self.currentExploreRoomCoordinator = coordinator
|
||||
|
||||
self.spaceIdStack.append(item.childInfo.childRoomId)
|
||||
self.spaceIdStack.append(spaceId)
|
||||
|
||||
self.navigationRouter.push(coordinator.toPresentable(), animated: true) {
|
||||
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 {
|
||||
MXLog.error("[ExploreRoomCoordinator] pushInviteScreen: room not found.")
|
||||
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)
|
||||
coordinator.delegate = self
|
||||
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) {
|
||||
|
@ -229,11 +277,19 @@ extension ExploreRoomCoordinator: SpaceExploreRoomCoordinatorDelegate {
|
|||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
.resizable()
|
||||
.frame(width: CGFloat(size.rawValue), height: CGFloat(size.rawValue))
|
||||
.clipShape(Circle())
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
}
|
||||
.onChange(of: displayName, perform: { value in
|
||||
|
|
|
@ -16,6 +16,20 @@
|
|||
|
||||
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
|
||||
@available(iOS 14.0, *)
|
||||
struct ClearViewModifier: ViewModifier
|
||||
|
|
|
@ -35,6 +35,7 @@ struct RoundedBorderTextEditor: View {
|
|||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
@Environment(\.isEnabled) private var isEnabled
|
||||
|
||||
// MARK: Setup
|
||||
|
||||
|
@ -73,18 +74,32 @@ struct RoundedBorderTextEditor: View {
|
|||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
ThemableTextEditor(text: $text, onEditingChanged: { edit in
|
||||
self.editing = edit
|
||||
onEditingChanged?(edit)
|
||||
})
|
||||
.modifier(ClearViewModifier(alignment: .top, text: $text))
|
||||
// Found no good solution here. Hidding next button for the moment
|
||||
// .modifier(NextViewModifier(alignment: .bottomTrailing, isEditing: $editing))
|
||||
.padding(EdgeInsets(top: 2, leading: 6, bottom: 0, trailing: 0))
|
||||
.onChange(of: text, perform: { newText in
|
||||
onTextChanged?(newText)
|
||||
})
|
||||
if isEnabled {
|
||||
ThemableTextEditor(text: $text, onEditingChanged: { edit in
|
||||
self.editing = edit
|
||||
onEditingChanged?(edit)
|
||||
})
|
||||
.showClearButton(text: $text)
|
||||
// Found no good solution here. Hidding next button for the moment
|
||||
// .modifier(NextViewModifier(alignment: .bottomTrailing, isEditing: $editing))
|
||||
.padding(EdgeInsets(top: 2, leading: 6, bottom: 0, trailing: 0))
|
||||
.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)
|
||||
.stroke(editing ? theme.colors.accent : (error == nil ? theme.colors.quinaryContent : theme.colors.alert), lineWidth: editing || error != nil ? 2 : 1))
|
||||
.frame(height: textMaxHeight)
|
||||
|
@ -108,18 +123,19 @@ struct ThemableTextEditor_Previews: PreviewProvider {
|
|||
static var previews: some View {
|
||||
|
||||
Group {
|
||||
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"))
|
||||
}
|
||||
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)
|
||||
sampleView.theme(.light).preferredColorScheme(.light)
|
||||
sampleView.theme(.dark).preferredColorScheme(.dark)
|
||||
}
|
||||
.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
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
@Environment(\.isEnabled) private var isEnabled
|
||||
|
||||
// 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.placeHolder = placeHolder
|
||||
self._text = text
|
||||
|
@ -72,18 +81,33 @@ struct RoundedBorderTextField: View {
|
|||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
.lineLimit(1)
|
||||
}
|
||||
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)
|
||||
.modifier(ClearViewModifier(alignment: .center, text: $text))
|
||||
if isEnabled {
|
||||
ThemableTextField(placeholder: "", text: $text, configuration: configuration, onEditingChanged: { edit in
|
||||
self.editing = edit
|
||||
onEditingChanged?(edit)
|
||||
})
|
||||
.makeFirstResponder(isFirstResponder)
|
||||
.showClearButton(text: $text)
|
||||
.onChange(of: text, perform: { newText in
|
||||
onTextChanged?(newText)
|
||||
})
|
||||
.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))
|
||||
.background(RoundedRectangle(cornerRadius: 8).fill(theme.colors.background))
|
||||
.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))
|
||||
|
||||
|
@ -107,20 +131,20 @@ struct TextFieldWithError_Previews: PreviewProvider {
|
|||
static var previews: some View {
|
||||
|
||||
Group {
|
||||
VStack(alignment: .center, spacing: 40) {
|
||||
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))
|
||||
}
|
||||
|
||||
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)
|
||||
sampleView.theme(.light).preferredColorScheme(.light)
|
||||
sampleView.theme(.dark).preferredColorScheme(.dark)
|
||||
}
|
||||
.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 {
|
||||
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
|
||||
guard let self = self else { return }
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ final class RoomAccessCoordinatorBridgePresenter: NSObject {
|
|||
// MARK: Private
|
||||
|
||||
private let room: MXRoom
|
||||
private let allowsRoomUpgrade: Bool
|
||||
private var coordinator: RoomAccessCoordinator?
|
||||
|
||||
// MARK: Public
|
||||
|
@ -42,16 +43,22 @@ final class RoomAccessCoordinatorBridgePresenter: NSObject {
|
|||
|
||||
// MARK: - Setup
|
||||
|
||||
init(room: MXRoom) {
|
||||
init(room: MXRoom,
|
||||
allowsRoomUpgrade: Bool) {
|
||||
self.room = room
|
||||
self.allowsRoomUpgrade = allowsRoomUpgrade
|
||||
super.init()
|
||||
}
|
||||
|
||||
convenience init(room: MXRoom) {
|
||||
self.init(room: room, allowsRoomUpgrade: true)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func present(from viewController: UIViewController, animated: Bool) {
|
||||
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
|
||||
guard let self = self else { return }
|
||||
|
||||
|
|
|
@ -23,13 +23,18 @@ struct RoomAccessCoordinatorParameters {
|
|||
|
||||
/// The Matrix room
|
||||
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
|
||||
let navigationRouter: NavigationRouterType
|
||||
|
||||
init(room: MXRoom,
|
||||
allowsRoomUpgrade: Bool = true,
|
||||
navigationRouter: NavigationRouterType? = nil) {
|
||||
self.room = room
|
||||
self.allowsRoomUpgrade = allowsRoomUpgrade
|
||||
self.navigationRouter = navigationRouter ?? NavigationRouter(navigationController: RiotNavigationController())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import SwiftUI
|
|||
|
||||
struct RoomAccessTypeChooserCoordinatorParameters {
|
||||
let roomId: String
|
||||
let allowsRoomUpgrade: Bool
|
||||
let session: MXSession
|
||||
}
|
||||
|
||||
|
@ -42,7 +43,7 @@ final class RoomAccessTypeChooserCoordinator: Coordinator, Presentable {
|
|||
@available(iOS 14.0, *)
|
||||
init(parameters: RoomAccessTypeChooserCoordinatorParameters) {
|
||||
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 view = RoomAccessTypeChooser(viewModel: viewModel.context, roomName: room?.displayName ?? "")
|
||||
roomAccessTypeChooserViewModel = viewModel
|
||||
|
|
|
@ -26,6 +26,7 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
|
|||
// MARK: Private
|
||||
|
||||
private let roomId: String
|
||||
private let allowsRoomUpgrade: Bool
|
||||
private let session: MXSession
|
||||
private var replacementRoom: MXRoom?
|
||||
private var didBuildSpaceGraphObserver: Any?
|
||||
|
@ -63,8 +64,9 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
|
|||
|
||||
// MARK: - Setup
|
||||
|
||||
init(roomId: String, session: MXSession) {
|
||||
init(roomId: String, allowsRoomUpgrade: Bool, session: MXSession) {
|
||||
self.roomId = roomId
|
||||
self.allowsRoomUpgrade = allowsRoomUpgrade
|
||||
self.session = session
|
||||
self.currentRoomId = roomId
|
||||
self.versionOverride = session.homeserverCapabilitiesService.versionOverrideForFeature(.restricted)
|
||||
|
@ -144,7 +146,7 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
|
|||
// MARK: - Private
|
||||
|
||||
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 = [
|
||||
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),
|
||||
|
|
|
@ -37,13 +37,13 @@ class MatrixItemChooserRoomRestrictedAllowedParentsDataSource: MatrixItemChooser
|
|||
guard let self = self else { return }
|
||||
|
||||
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
|
||||
guard let type = allowDictionnary["type"], type == "m.room_membership" else {
|
||||
guard let type = allowDictionnary[kMXJoinRulesContentKeyType], type == kMXEventTypeStringRoomMembership else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return allowDictionnary["room_id"]
|
||||
return allowDictionnary[kMXJoinRulesContentKeyRoomId]
|
||||
}
|
||||
|
||||
let ancestorsId = session.spaceService.ancestorsPerRoomId[self.roomId] ?? []
|
||||
|
|
|
@ -18,7 +18,32 @@ import Foundation
|
|||
|
||||
enum SpaceCreationSettingsAddressValidationStatus {
|
||||
case none(_ address: String)
|
||||
case current(_ address: String)
|
||||
case valid(_ address: String)
|
||||
case alreadyExists(_ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@ class SpaceCreationSettingsService: SpaceCreationSettingsServiceProtocol {
|
|||
}
|
||||
|
||||
private func updateDefaultAddress() {
|
||||
defaultAddress = roomName.toValidAliasLocalPart()
|
||||
defaultAddress = MXTools.validAliasLocalPart(from: roomName)
|
||||
}
|
||||
|
||||
private func validateAddress() {
|
||||
|
@ -102,7 +102,7 @@ class SpaceCreationSettingsService: SpaceCreationSettingsServiceProtocol {
|
|||
currentOperation = nil
|
||||
|
||||
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 {
|
||||
addressValidationSubject.send(.none(fullAddress))
|
||||
|
@ -116,7 +116,7 @@ class SpaceCreationSettingsService: SpaceCreationSettingsServiceProtocol {
|
|||
}
|
||||
|
||||
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
|
||||
guard let self = self else { return }
|
||||
|
|
|
@ -76,8 +76,8 @@ class SpaceCreationSettingsViewModel: SpaceCreationSettingsViewModelType, SpaceC
|
|||
showRoomAddress: creationParameters.showAddress,
|
||||
defaultAddress: creationParameters.address ?? "",
|
||||
roomNameError: nil,
|
||||
addressMessage: addressMessage(with: validationStatus),
|
||||
isAddressValid: isAddressValid(with: validationStatus),
|
||||
addressMessage: validationStatus.message,
|
||||
isAddressValid: validationStatus.isValid,
|
||||
avatar: AvatarInput(mxContentUri: nil, matrixItemId: "", displayName: nil),
|
||||
avatarImage: creationParameters.userSelectedAvatar,
|
||||
bindings: bindings)
|
||||
|
@ -120,8 +120,8 @@ class SpaceCreationSettingsViewModel: SpaceCreationSettingsViewModelType, SpaceC
|
|||
case .updateRoomDefaultAddress(let defaultAddress):
|
||||
state.defaultAddress = defaultAddress
|
||||
case .updateAddressValidationStatus(let validationStatus):
|
||||
state.addressMessage = Self.addressMessage(with: validationStatus)
|
||||
state.isAddressValid = Self.isAddressValid(with: validationStatus)
|
||||
state.addressMessage = validationStatus.message
|
||||
state.isAddressValid = validationStatus.isValid
|
||||
case .updateAvatar(let avatar):
|
||||
state.avatar = avatar
|
||||
case .updateAvatarImage(let image):
|
||||
|
@ -161,26 +161,4 @@ class SpaceCreationSettingsViewModel: SpaceCreationSettingsViewModelType, SpaceC
|
|||
private func pickImage(from sourceRect: CGRect) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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?)
|
||||
}
|
|
@ -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") }
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
1
changelog.d/5233.feature
Normal file
1
changelog.d/5233.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Space Settings
|
Loading…
Reference in a new issue