diff --git a/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/Contents.json new file mode 100644 index 000000000..6bb7945f7 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "space_private_icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "space_private_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "space_private_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon.png b/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon.png new file mode 100644 index 000000000..10bc95ab8 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon.png differ diff --git a/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon@2x.png b/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon@2x.png new file mode 100644 index 000000000..cae6344e1 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon@3x.png b/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon@3x.png new file mode 100644 index 000000000..eaf48592a Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon@3x.png differ diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 32c50a8e7..0ff445f39 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -1718,6 +1718,7 @@ Tap the + to start adding people."; "spaces_coming_soon_detail" = "This feature hasn’t been implemented here, but it’s on the way. For now, you can do that with Element on your computer."; "space_participants_action_remove" = "Remove from this space"; "space_participants_action_ban" = "Ban from this space"; +"space_home_show_all_rooms" = "Show all rooms"; "space_private_join_rule" = "Private space"; "space_public_join_rule" = "Public space"; diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 2af5367f5..b6f7f626c 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -192,6 +192,7 @@ internal enum Asset { internal static let spaceMenuLeave = ImageAsset(name: "space_menu_leave") internal static let spaceMenuMembers = ImageAsset(name: "space_menu_members") internal static let spaceMenuRooms = ImageAsset(name: "space_menu_rooms") + internal static let spacePrivateIcon = ImageAsset(name: "space_private_icon") internal static let spaceRoomIcon = ImageAsset(name: "space_room_icon") internal static let spaceTypeIcon = ImageAsset(name: "space_type_icon") internal static let spaceUserIcon = ImageAsset(name: "space_user_icon") @@ -222,6 +223,7 @@ internal struct ImageAsset { internal typealias Image = UIImage #endif + @available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *) internal var image: Image { let bundle = BundleToken.bundle #if os(iOS) || os(tvOS) @@ -233,13 +235,25 @@ internal struct ImageAsset { let image = Image(named: name) #endif guard let result = image else { - fatalError("Unable to load image named \(name).") + fatalError("Unable to load image asset named \(name).") } return result } + + #if os(iOS) || os(tvOS) + @available(iOS 8.0, tvOS 9.0, *) + internal func image(compatibleWith traitCollection: UITraitCollection) -> Image { + let bundle = BundleToken.bundle + guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else { + fatalError("Unable to load image asset named \(name).") + } + return result + } + #endif } internal extension ImageAsset.Image { + @available(iOS 8.0, tvOS 9.0, watchOS 2.0, *) @available(macOS, deprecated, message: "This initializer is unsafe on macOS, please use the ImageAsset.image property") convenience init!(asset: ImageAsset) { diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index d82e45c6f..e53eed47b 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4855,6 +4855,10 @@ public class VectorL10n: NSObject { public static var spaceFeatureUnavailableTitle: String { return VectorL10n.tr("Vector", "space_feature_unavailable_title") } + /// Show all rooms + public static var spaceHomeShowAllRooms: String { + return VectorL10n.tr("Vector", "space_home_show_all_rooms") + } /// Ban from this space public static var spaceParticipantsActionBan: String { return VectorL10n.tr("Vector", "space_participants_action_ban") diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m index a219ab3c9..cdd7d014e 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -1610,7 +1610,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou } MXLogDebug(@"[RecentsDataSource] refreshRoomsSections: Done in %.0fms", [[NSDate date] timeIntervalSinceDate:startDate] * 1000); - MXLogDebug(@"[Spaces] refreshRoomsSections with %ld suggested room", suggestedRoomCellDataArray.count); + MXLogDebug(@"[RecentsDataSource] refreshRoomsSections with %ld suggested room", suggestedRoomCellDataArray.count); return [[RecentsDataSourceState alloc] initWithInvitesCellDataArray:invitesCellDataArray diff --git a/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewController.swift b/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewController.swift index 95f7e7b19..bca282949 100644 --- a/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewController.swift +++ b/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewController.swift @@ -231,6 +231,9 @@ class SpaceDetailViewController: UIViewController { let membersString = membersCount == 1 ? VectorL10n.roomTitleOneMember : VectorL10n.roomTitleMembers("\(membersCount)") self.spaceTypeLabel.text = "\(joinRuleString) · \(membersString)" + let joinRuleIcon = parameters.joinRule == .public ? Asset.Images.spaceTypeIcon : Asset.Images.spacePrivateIcon + self.spaceTypeIconView.image = joinRuleIcon.image + self.inviterIdLabel.text = parameters.inviterId if let inviterId = parameters.inviterId { self.inviterTitleLabel.text = "\(parameters.inviter?.displayname ?? inviterId) invited you" diff --git a/Riot/Modules/Spaces/SpaceList/SpaceListViewCell.swift b/Riot/Modules/Spaces/SpaceList/SpaceListViewCell.swift index d0ea314dd..eaf3a5e09 100644 --- a/Riot/Modules/Spaces/SpaceList/SpaceListViewCell.swift +++ b/Riot/Modules/Spaces/SpaceList/SpaceListViewCell.swift @@ -58,7 +58,7 @@ final class SpaceListViewCell: UITableViewCell, Themable, NibReusable { func fill(with viewData: SpaceListItemViewData) { self.avatarView.fill(with: viewData.avatarViewData) self.titleLabel.text = viewData.title - self.moreButton.isHidden = viewData.spaceId == SpaceListViewModel.Constants.homeSpaceId || viewData.isInvite + self.moreButton.isHidden = viewData.isInvite if viewData.isInvite { self.isBadgeAlert = true self.badgeLabel.isHidden = false diff --git a/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift b/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift index ed11d2df9..3f9924531 100644 --- a/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift +++ b/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift @@ -91,7 +91,8 @@ final class SpaceListViewModel: SpaceListViewModelType { case .moreAction(at: let indexPath, from: let sourceView): let section = self.sections[indexPath.section] switch section { - case .home: break + case .home: + self.coordinatorDelegate?.spaceListViewModel(self, didPressMoreForSpaceWithId: Constants.homeSpaceId, from: sourceView) case .spaces(let viewDataList): let spaceViewData = viewDataList[indexPath.row] self.coordinatorDelegate?.spaceListViewModel(self, didPressMoreForSpaceWithId: spaceViewData.spaceId, from: sourceView) diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuCell.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuCell.swift new file mode 100644 index 000000000..2447ae918 --- /dev/null +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuCell.swift @@ -0,0 +1,21 @@ +// +// 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 SpaceMenuCell: Themable { + func fill(with viewData: SpaceMenuListItemViewData) +} diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift index 6b7a67fa5..ca555e580 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift @@ -19,6 +19,7 @@ import Foundation /// Style of the `SpaceMenuListViewCell` enum SpaceMenuListItemStyle { case normal + case boolean case destructive } @@ -28,4 +29,5 @@ struct SpaceMenuListItemViewData { let style: SpaceMenuListItemStyle let title: String? let icon: UIImage? + var value: Any? } diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.swift index 952d89d4d..86bfe8805 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListViewCell.swift @@ -17,7 +17,7 @@ import Foundation import Reusable -class SpaceMenuListViewCell: UITableViewCell, Themable, NibReusable { +class SpaceMenuListViewCell: UITableViewCell, SpaceMenuCell, NibReusable { // MARK: - Properties diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.swift new file mode 100644 index 000000000..6cd7a7a39 --- /dev/null +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.swift @@ -0,0 +1,74 @@ +// +// 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 Reusable + +class SpaceMenuSwitchViewCell: UITableViewCell, SpaceMenuCell, NibReusable { + + // MARK: - Properties + + @IBOutlet private weak var titleLabel: UILabel! + @IBOutlet private weak var selectionView: UIView! + @IBOutlet private weak var switchView: UISwitch! + + // MARK: - Private + + private var theme: Theme? + + // MARK: - Life cycle + + override func awakeFromNib() { + super.awakeFromNib() + + self.selectionStyle = .none + self.selectionView.layer.cornerRadius = 8.0 + self.selectionView.layer.masksToBounds = true + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + UIView.animate(withDuration: animated ? 0.3 : 0.0) { + self.selectionView.alpha = selected ? 1.0 : 0.0 + } + } + + // MARK: - Public + + func fill(with viewData: SpaceMenuListItemViewData) { + self.titleLabel.text = viewData.title + self.switchView.isOn = (viewData.value as? Bool) ?? false + + guard let theme = self.theme else { + return + } + + if viewData.style == .destructive { + self.titleLabel.textColor = theme.colors.alert + } else { + self.titleLabel.textColor = theme.colors.primaryContent + } + } + + func update(theme: Theme) { + self.theme = theme + self.backgroundColor = theme.colors.background + self.titleLabel.textColor = theme.colors.primaryContent + self.titleLabel.font = theme.fonts.body + self.selectionView.backgroundColor = theme.colors.separator + } +} diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.xib b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.xib new file mode 100644 index 000000000..f652ee4b7 --- /dev/null +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuSwitchViewCell.xib @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift index 8f9710e56..9cb954398 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift @@ -114,6 +114,17 @@ class SpaceMenuViewController: UIViewController { } private func setupViews() { + setupTableView() + + if self.spaceId == SpaceListViewModel.Constants.homeSpaceId { + let avatarViewData = AvatarViewData(matrixItemId: self.spaceId, displayName: nil, avatarUrl: nil, mediaManager: session.mediaManager, fallbackImage: .image(Asset.Images.spaceHomeIcon.image, .center)) + self.avatarView.fill(with: avatarViewData) + self.titleLabel.text = VectorL10n.titleHome + self.subtitleLabel.text = VectorL10n.settingsTitle + + return + } + guard let space = self.session.spaceService.getSpace(withId: self.spaceId), let summary = space.summary else { MXLog.error("[SpaceMenuViewController] setupViews: no space found") return @@ -130,7 +141,6 @@ class SpaceMenuViewController: UIViewController { self.closeButton.layer.masksToBounds = true self.closeButton.layer.cornerRadius = self.closeButton.bounds.height / 2 - setupTableView() } private func setupTableView() { @@ -139,6 +149,7 @@ class SpaceMenuViewController: UIViewController { self.tableView.estimatedRowHeight = Constants.estimatedRowHeight self.tableView.allowsSelection = true self.tableView.register(cellType: SpaceMenuListViewCell.self) + self.tableView.register(cellType: SpaceMenuSwitchViewCell.self) self.tableView.tableFooterView = UIView() } @@ -150,6 +161,8 @@ class SpaceMenuViewController: UIViewController { self.renderLoaded() case .leaveOptions(let displayName, let isAdmin): self.renderLeaveOptions(displayName: displayName, isAdmin: isAdmin) + case .updateItem(let indexPath): + self.tableView.reloadRows(at: [indexPath], with: .fade) case .error(let error): self.render(error: error) case .deselect: @@ -235,12 +248,15 @@ extension SpaceMenuViewController: UITableViewDataSource { } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(for: indexPath, cellType: SpaceMenuListViewCell.self) - let viewData = viewModel.menuItems[indexPath.row] - cell.update(theme: self.theme) - cell.fill(with: viewData) + let cell = viewData.style == .boolean ? tableView.dequeueReusableCell(for: indexPath, cellType: SpaceMenuSwitchViewCell.self) : + tableView.dequeueReusableCell(for: indexPath, cellType: SpaceMenuListViewCell.self) + + if let cell = cell as? SpaceMenuCell { + cell.update(theme: self.theme) + cell.fill(with: viewData) + } return cell } diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift index be6ad2514..cd3c99f9c 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift @@ -22,6 +22,7 @@ class SpaceMenuViewModel: SpaceMenuViewModelType { // MARK: - Enum enum ActionId: String { + case showAllRoomsInHome = "showAllRoomsInHome" case members = "members" case rooms = "rooms" case leave = "leave" @@ -32,12 +33,14 @@ class SpaceMenuViewModel: SpaceMenuViewModelType { weak var coordinatorDelegate: SpaceMenuModelViewModelCoordinatorDelegate? weak var viewDelegate: SpaceMenuViewModelViewDelegate? - var menuItems: [SpaceMenuListItemViewData] = [ - SpaceMenuListItemViewData(actionId: ActionId.members.rawValue, style: .normal, title: VectorL10n.roomDetailsPeople, icon: UIImage(named: "space_menu_members")), - SpaceMenuListItemViewData(actionId: ActionId.rooms.rawValue, style: .normal, title: VectorL10n.spacesExploreRooms, icon: UIImage(named: "space_menu_rooms")), - SpaceMenuListItemViewData(actionId: ActionId.leave.rawValue, style: .destructive, title: VectorL10n.leave, icon: UIImage(named: "space_menu_leave")) + private let spaceMenuItems: [SpaceMenuListItemViewData] = [ + SpaceMenuListItemViewData(actionId: ActionId.members.rawValue, style: .normal, title: VectorL10n.roomDetailsPeople, icon: Asset.Images.spaceMenuMembers.image, value: nil), + SpaceMenuListItemViewData(actionId: ActionId.rooms.rawValue, style: .normal, title: VectorL10n.spacesExploreRooms, icon: Asset.Images.spaceMenuRooms.image, value: nil), + SpaceMenuListItemViewData(actionId: ActionId.leave.rawValue, style: .destructive, title: VectorL10n.leave, icon: Asset.Images.spaceMenuLeave.image, value: nil) ] + var menuItems: [SpaceMenuListItemViewData] = [] + private let session: MXSession private let spaceId: String @@ -46,6 +49,14 @@ class SpaceMenuViewModel: SpaceMenuViewModelType { init(session: MXSession, spaceId: String) { self.session = session self.spaceId = spaceId + + if spaceId != SpaceListViewModel.Constants.homeSpaceId { + self.menuItems = spaceMenuItems + } else { + self.menuItems = [ + SpaceMenuListItemViewData(actionId: ActionId.showAllRoomsInHome.rawValue, style: .boolean, title: VectorL10n.spaceHomeShowAllRooms, icon: nil, value: MXKAppSettings.standard().isShowAllRoomsInHomeEnabled) + ] + } } // MARK: - Public @@ -55,7 +66,7 @@ class SpaceMenuViewModel: SpaceMenuViewModelType { case .dismiss: self.coordinatorDelegate?.spaceMenuViewModelDidDismiss(self) case .selectRow(at: let indexPath): - self.processAction(with: menuItems[indexPath.row].actionId) + self.processAction(with: menuItems[indexPath.row].actionId, at: indexPath) case .leaveSpaceAndKeepRooms: self.leaveSpaceAndKeepRooms() case .leaveSpaceAndLeaveRooms: @@ -65,9 +76,14 @@ class SpaceMenuViewModel: SpaceMenuViewModelType { // MARK: - Private - private func processAction(with actionStringId: String) { + private func processAction(with actionStringId: String, at indexPath: IndexPath) { let actionId = ActionId(rawValue: actionStringId) switch actionId { + case .showAllRoomsInHome: + MXKAppSettings.standard().isShowAllRoomsInHomeEnabled = !MXKAppSettings.standard().isShowAllRoomsInHomeEnabled + self.menuItems[indexPath.row].value = MXKAppSettings.standard().isShowAllRoomsInHomeEnabled + self.viewDelegate?.spaceMenuViewModel(self, didUpdateViewState: .deselect) + self.viewDelegate?.spaceMenuViewModel(self, didUpdateViewState: .updateItem(indexPath)) case .leave: self.leaveSpace() default: diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewState.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewState.swift index 9801ffaf5..0e37aad97 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewState.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewState.swift @@ -21,6 +21,7 @@ enum SpaceMenuViewState { case loading case loaded case deselect + case updateItem(_ indexPath: IndexPath) case leaveOptions(_ displayName: String, _ isAdmin: Bool) case error(Error) }