diff --git a/Riot/Categories/MXSession+Riot.h b/Riot/Categories/MXSession+Riot.h index d5fe928de..168511ec2 100644 --- a/Riot/Categories/MXSession+Riot.h +++ b/Riot/Categories/MXSession+Riot.h @@ -44,4 +44,7 @@ Return the homeserver configuration based on HS Well-Known or BuildSettings prop */ - (BOOL)vc_canSetupSecureBackup; +// TODO: Move to SDK +- (MXRoom*)vc_roomWithIdOrAlias:(NSString*)roomIdOrAlias; + @end diff --git a/Riot/Categories/MXSession+Riot.m b/Riot/Categories/MXSession+Riot.m index 1cc498024..901c58406 100644 --- a/Riot/Categories/MXSession+Riot.m +++ b/Riot/Categories/MXSession+Riot.m @@ -92,4 +92,15 @@ == crossSigningServiceSecrets.count); } +- (MXRoom*)vc_roomWithIdOrAlias:(NSString*)roomIdOrAlias +{ + if ([MXTools isMatrixRoomIdentifier:roomIdOrAlias]) { + return [self roomWithRoomId:roomIdOrAlias]; + } else if ([MXTools isMatrixRoomAlias:roomIdOrAlias]) { + return [self roomWithAlias:roomIdOrAlias]; + } else { + return nil; + } +} + @end diff --git a/Riot/Modules/Rooms/ShowDirectory/PublicRoomsDirectoryViewModel.swift b/Riot/Modules/Rooms/ShowDirectory/PublicRoomsDirectoryViewModel.swift new file mode 100644 index 000000000..28074b89b --- /dev/null +++ b/Riot/Modules/Rooms/ShowDirectory/PublicRoomsDirectoryViewModel.swift @@ -0,0 +1,69 @@ +// +// 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 + +class PublicRoomsDirectoryViewModel { + + // MARK: - Properties + + // MARK: Private + + private let dataSource: PublicRoomsDirectoryDataSource + private let session: MXSession + + // MARK: Public + + var roomsCount: Int { + return Int(dataSource.roomsCount) + } + + var directoryServerDisplayname: String? { + return dataSource.directoryServerDisplayname + } + + // MARK: - Setup + + init(dataSource: PublicRoomsDirectoryDataSource, session: MXSession) { + self.dataSource = dataSource + self.session = session + } + + // MARK: - Public + + func roomViewModel(at row: Int) -> DirectoryRoomTableViewCellVM? { + self.roomViewModel(at: IndexPath(row: row, section: 0)) + } + + func roomViewModel(at indexPath: IndexPath) -> DirectoryRoomTableViewCellVM? { + guard let publicRoom = dataSource.room(at: indexPath) else { return nil } + return self.roomCellViewModel(with: publicRoom) + } + + // MARK: - Private + + private func roomCellViewModel(with publicRoom: MXPublicRoom) -> DirectoryRoomTableViewCellVM { + let summary = session.roomSummary(withRoomId: publicRoom.roomId) + + return DirectoryRoomTableViewCellVM(title: publicRoom.displayname(), + numberOfUsers: publicRoom.numJoinedMembers, + subtitle: MXTools.stripNewlineCharacters(publicRoom.topic), + isJoined: summary?.membership == .join, + roomId: publicRoom.roomId, + avatarUrl: publicRoom.avatarUrl, + mediaManager: session.mediaManager) + } +} diff --git a/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewController.swift b/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewController.swift index 38fdbc738..a9c9362e7 100644 --- a/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewController.swift +++ b/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewController.swift @@ -62,6 +62,8 @@ final class ShowDirectoryViewController: UIViewController { bar.delegate = self return bar }() + + private var sections: [ShowDirectorySection] = [] // MARK: - Setup @@ -171,10 +173,12 @@ final class ShowDirectoryViewController: UIViewController { switch viewState { case .loading: self.renderLoading() - case .loaded: - self.renderLoaded() + case .loaded(let sections): + self.renderLoaded(sections: sections) case .error(let error): self.render(error: error) + case .loadedWithoutUpdate: + self.renderLoadedWithoutUpdate() } } @@ -182,14 +186,22 @@ final class ShowDirectoryViewController: UIViewController { addSpinnerFooterView() } - private func renderLoaded() { + private func renderLoaded(sections: [ShowDirectorySection]) { + removeSpinnerFooterView() + self.sections = sections + self.mainTableView.reloadData() + } + + private func renderLoadedWithoutUpdate() { removeSpinnerFooterView() - } private func render(error: Error) { removeSpinnerFooterView() - self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil) + self.errorPresenter.presentError(from: self, forError: error, animated: true) { + // If the join failed, reload the table view + self.mainTableView.reloadData() + } } // MARK: - Actions @@ -209,17 +221,37 @@ final class ShowDirectoryViewController: UIViewController { extension ShowDirectoryViewController: UITableViewDataSource { func numberOfSections(in tableView: UITableView) -> Int { - return 1 + return self.sections.count } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return viewModel.roomsCount + + let directorySection = self.sections[section] + + switch directorySection { + case .searchInput: + return 1 + case.publicRoomsDirectory(let viewModel): + return viewModel.roomsCount + } } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + let section = self.sections[indexPath.section] + + let cellViewModel: DirectoryRoomTableViewCellVM? + + switch section { + case .searchInput(let searchInputViewData): + cellViewModel = searchInputViewData + case.publicRoomsDirectory(let viewModel): + cellViewModel = viewModel.roomViewModel(at: indexPath.row) + } + let cell: DirectoryRoomTableViewCell = tableView.dequeueReusableCell(for: indexPath) - if let viewModel = viewModel.roomViewModel(at: indexPath) { - cell.configure(withViewModel: viewModel) + if let cellViewModel = cellViewModel { + cell.configure(withViewModel: cellViewModel) } cell.indexPath = indexPath cell.delegate = self @@ -255,16 +287,28 @@ extension ShowDirectoryViewController: UITableViewDelegate { } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - guard let view: DirectoryNetworkTableHeaderFooterView = tableView.dequeueReusableHeaderFooterView() else { - return nil + + let sectionHeaderView: UIView? + + let directorySection = self.sections[section] + + switch directorySection { + case .searchInput: + sectionHeaderView = nil + case .publicRoomsDirectory(let viewModel): + guard let view: DirectoryNetworkTableHeaderFooterView = tableView.dequeueReusableHeaderFooterView() else { + return nil + } + if let name = viewModel.directoryServerDisplayname { + let title = VectorL10n.searchableDirectoryXNetwork(name) + view.configure(withViewModel: DirectoryNetworkVM(title: title)) + } + view.update(theme: self.theme) + view.delegate = self + sectionHeaderView = view } - if let name = self.viewModel.directoryServerDisplayname { - let title = VectorL10n.searchableDirectoryXNetwork(name) - view.configure(withViewModel: DirectoryNetworkVM(title: title)) - } - view.update(theme: self.theme) - view.delegate = self - return view + + return sectionHeaderView } func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { @@ -311,8 +355,4 @@ extension ShowDirectoryViewController: ShowDirectoryViewModelViewDelegate { func showDirectoryViewModel(_ viewModel: ShowDirectoryViewModelType, didUpdateViewState viewSate: ShowDirectoryViewState) { self.render(viewState: viewSate) } - - func showDirectoryViewModelDidUpdateDataSource(_ viewModel: ShowDirectoryViewModelType) { - self.mainTableView.reloadData() - } } diff --git a/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewModel.swift b/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewModel.swift index 54c65ba90..55d53047e 100644 --- a/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewModel.swift +++ b/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewModel.swift @@ -18,6 +18,11 @@ import Foundation +enum ShowDirectorySection { + case searchInput(_ searchInputViewData: DirectoryRoomTableViewCellVM) + case publicRoomsDirectory(_ viewModel: PublicRoomsDirectoryViewModel) +} + final class ShowDirectoryViewModel: NSObject, ShowDirectoryViewModelType { // MARK: - Properties @@ -27,38 +32,30 @@ final class ShowDirectoryViewModel: NSObject, ShowDirectoryViewModelType { private let session: MXSession private let dataSource: PublicRoomsDirectoryDataSource + private let publicRoomsDirectoryViewModel: PublicRoomsDirectoryViewModel + private var currentOperation: MXHTTPOperation? - private var userDisplayName: String? + private var sections: [ShowDirectorySection] = [] + + private var canPaginatePublicRoomsDirectory: Bool { + return !dataSource.hasReachedPaginationEnd && currentOperation == nil + } + + private var publicRoomsDirectorySection: ShowDirectorySection { + return .publicRoomsDirectory(self.publicRoomsDirectoryViewModel) + } // MARK: Public weak var viewDelegate: ShowDirectoryViewModelViewDelegate? weak var coordinatorDelegate: ShowDirectoryViewModelCoordinatorDelegate? - var roomsCount: Int { - return Int(dataSource.roomsCount) - } - var directoryServerDisplayname: String? { - return dataSource.directoryServerDisplayname - } - func roomViewModel(at indexPath: IndexPath) -> DirectoryRoomTableViewCellVM? { - guard let room = dataSource.room(at: indexPath) else { return nil } - let summary = session.roomSummary(withRoomId: room.roomId) - - return DirectoryRoomTableViewCellVM(title: room.displayname(), - numberOfUsers: room.numJoinedMembers, - subtitle: MXTools.stripNewlineCharacters(room.topic), - isJoined: summary?.membership == .join, - roomId: room.roomId, - avatarUrl: room.avatarUrl, - mediaManager: session.mediaManager) - } - // MARK: - Setup init(session: MXSession, dataSource: PublicRoomsDirectoryDataSource) { self.session = session self.dataSource = dataSource + self.publicRoomsDirectoryViewModel = PublicRoomsDirectoryViewModel(dataSource: dataSource, session: session) } deinit { @@ -69,17 +66,38 @@ final class ShowDirectoryViewModel: NSObject, ShowDirectoryViewModelType { func process(viewAction: ShowDirectoryViewAction) { switch viewAction { - case .loadData(let force): - self.loadData(force: force) + case .loadData: + self.resetSections() + self.paginatePublicRoomsDirectory(force: false) case .selectRoom(let indexPath): - guard let room = dataSource.room(at: indexPath) else { return } - self.coordinatorDelegate?.showDirectoryViewModelDidSelect(self, room: room) + + let directorySection = self.sections[indexPath.section] + + switch directorySection { + case .searchInput: + break + case.publicRoomsDirectory: + guard let publicRoom = dataSource.room(at: indexPath) else { return } + self.coordinatorDelegate?.showDirectoryViewModelDidSelect(self, room: publicRoom) + } case .joinRoom(let indexPath): - guard let room = dataSource.room(at: indexPath) else { return } - joinRoom(room) + + let directorySection = self.sections[indexPath.section] + let roomIdOrAlias: String? + + switch directorySection { + case .searchInput(let searchInputViewData): + roomIdOrAlias = searchInputViewData.title + case .publicRoomsDirectory: + let publicRoom = dataSource.room(at: IndexPath(row: indexPath.row, section: 0)) + roomIdOrAlias = publicRoom?.roomId + } + + if let roomIdOrAlias = roomIdOrAlias { + joinRoom(withRoomIdOrAlias: roomIdOrAlias) + } case .search(let pattern): - self.dataSource.searchPattern = pattern - self.loadData(force: true) + self.search(with: pattern) case .createNewRoom: self.coordinatorDelegate?.showDirectoryViewModelDidTapCreateNewRoom(self) case .switchServer: @@ -90,10 +108,22 @@ final class ShowDirectoryViewModel: NSObject, ShowDirectoryViewModelType { } } + func updatePublicRoomsDataSource(with cellData: MXKDirectoryServerCellDataStoring) { + if let thirdpartyProtocolInstance = cellData.thirdPartyProtocolInstance { + self.dataSource.thirdpartyProtocolInstance = thirdpartyProtocolInstance + } else if let homeserver = cellData.homeserver { + self.dataSource.includeAllNetworks = cellData.includeAllNetworks + self.dataSource.homeserver = homeserver + } + + self.resetSections() + self.paginatePublicRoomsDirectory(force: false) + } + // MARK: - Private - private func loadData(force: Bool) { - if !force && (dataSource.hasReachedPaginationEnd || currentOperation != nil) { + private func paginatePublicRoomsDirectory(force: Bool) { + if !force && !self.canPaginatePublicRoomsDirectory { // We got all public rooms or we are already paginating // Do nothing return @@ -101,12 +131,16 @@ final class ShowDirectoryViewModel: NSObject, ShowDirectoryViewModelType { self.update(viewState: .loading) + // Useful only when force is true + self.cancelOperations() + currentOperation = dataSource.paginate({ [weak self] (roomsAdded) in guard let self = self else { return } if roomsAdded > 0 { - self.viewDelegate?.showDirectoryViewModelDidUpdateDataSource(self) + self.update(viewState: .loaded(self.sections)) + } else { + self.update(viewState: .loadedWithoutUpdate) } - self.update(viewState: .loaded) self.currentOperation = nil }, failure: { [weak self] (error) in guard let self = self else { return } @@ -116,6 +150,12 @@ final class ShowDirectoryViewModel: NSObject, ShowDirectoryViewModelType { }) } + private func resetSections() { + self.sections = [self.publicRoomsDirectorySection] + } + + // FIXME: DirectoryServerPickerViewController should be instantiated from ShowDirectoryCoordinator + // It should be just a call like: self.coordinatorDelegate?.showDirectoryServerPicker(self) private func switchServer() { let controller = DirectoryServerPickerViewController() let source = MXKDirectoryServersDataSource(matrixSession: session) @@ -126,26 +166,85 @@ final class ShowDirectoryViewModel: NSObject, ShowDirectoryViewModelType { guard let self = self else { return } guard let cellData = cellData else { return } - if let thirdpartyProtocolInstance = cellData.thirdPartyProtocolInstance { - self.dataSource.thirdpartyProtocolInstance = thirdpartyProtocolInstance - } else if let homeserver = cellData.homeserver { - self.dataSource.includeAllNetworks = cellData.includeAllNetworks - self.dataSource.homeserver = homeserver - } - - self.loadData(force: false) + self.updatePublicRoomsDataSource(with: cellData) } self.coordinatorDelegate?.showDirectoryViewModelWantsToShow(self, controller: controller) } - private func joinRoom(_ room: MXPublicRoom) { - session.joinRoom(room.roomId) { [weak self] (response) in + private func joinRoom(withRoomIdOrAlias roomIdOrAlias: String) { + session.joinRoom(roomIdOrAlias) { [weak self] (response) in guard let self = self else { return } - self.viewDelegate?.showDirectoryViewModelDidUpdateDataSource(self) + switch response { + case .success: + self.update(viewState: .loaded(self.sections)) + case .failure(let error): + self.update(viewState: .error(error)) + } } } + private func search(with pattern: String?) { + self.dataSource.searchPattern = pattern + + var sections: [ShowDirectorySection] = [] + + var shouldUpdate = false + + // If the search text is a room id or alias we add search input entry in sections + if let searchText = pattern, let searchInputViewData = self.searchInputViewData(from: searchText) { + sections.append(.searchInput(searchInputViewData)) + + shouldUpdate = true + } + + sections.append(self.publicRoomsDirectorySection) + + self.sections = sections + + if shouldUpdate { + self.update(viewState: .loaded(self.sections)) + } + + self.paginatePublicRoomsDirectory(force: true) + } + + private func searchInputViewData(from searchText: String) -> DirectoryRoomTableViewCellVM? { + guard MXTools.isMatrixRoomAlias(searchText) || MXTools.isMatrixRoomIdentifier(searchText) else { + return nil + } + + let roomIdOrAlias = searchText + + let searchInputViewData: DirectoryRoomTableViewCellVM + + if let room = self.session.vc_room(withIdOrAlias: roomIdOrAlias) { + searchInputViewData = self.roomCellViewModel(with: room) + } else { + searchInputViewData = self.roomCellViewModel(with: roomIdOrAlias) + } + + return searchInputViewData + } + + private func roomCellViewModel(with room: MXRoom) -> DirectoryRoomTableViewCellVM { + let displayName = room.summary.displayname + let joinedMembersCount = Int(room.summary.membersCount.joined) + let topic = MXTools.stripNewlineCharacters(room.summary.topic) + let isJoined = room.summary.membership == .join + let avatarStringUrl = room.summary.avatar + let mediaManager = self.session.mediaManager + + return DirectoryRoomTableViewCellVM(title: displayName, numberOfUsers: joinedMembersCount, subtitle: topic, isJoined: isJoined, roomId: room.roomId, avatarUrl: avatarStringUrl, mediaManager: mediaManager) + } + + private func roomCellViewModel(with roomIdOrAlias: String) -> DirectoryRoomTableViewCellVM { + let displayName = roomIdOrAlias + let mediaManager = self.session.mediaManager + + return DirectoryRoomTableViewCellVM(title: displayName, numberOfUsers: 0, subtitle: nil, isJoined: false, roomId: roomIdOrAlias, avatarUrl: nil, mediaManager: mediaManager) + } + private func update(viewState: ShowDirectoryViewState) { self.viewDelegate?.showDirectoryViewModel(self, didUpdateViewState: viewState) } @@ -172,7 +271,7 @@ extension ShowDirectoryViewModel: MXKDataSourceDelegate { } func dataSource(_ dataSource: MXKDataSource!, didStateChange state: MXKDataSourceState) { - self.viewDelegate?.showDirectoryViewModelDidUpdateDataSource(self) + self.update(viewState: .loaded(self.sections)) } } diff --git a/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewModelType.swift b/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewModelType.swift index bae7c7633..6a91e82fc 100644 --- a/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewModelType.swift +++ b/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewModelType.swift @@ -20,7 +20,6 @@ import Foundation protocol ShowDirectoryViewModelViewDelegate: class { func showDirectoryViewModel(_ viewModel: ShowDirectoryViewModelType, didUpdateViewState viewSate: ShowDirectoryViewState) - func showDirectoryViewModelDidUpdateDataSource(_ viewModel: ShowDirectoryViewModelType) } protocol ShowDirectoryViewModelCoordinatorDelegate: class { @@ -38,7 +37,5 @@ protocol ShowDirectoryViewModelType { func process(viewAction: ShowDirectoryViewAction) - var roomsCount: Int { get } - var directoryServerDisplayname: String? { get } - func roomViewModel(at indexPath: IndexPath) -> DirectoryRoomTableViewCellVM? + func updatePublicRoomsDataSource(with cellData: MXKDirectoryServerCellDataStoring) } diff --git a/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewState.swift b/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewState.swift index 4de5b0886..46bc367ad 100644 --- a/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewState.swift +++ b/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewState.swift @@ -21,6 +21,7 @@ import Foundation /// ShowDirectoryViewController view state enum ShowDirectoryViewState { case loading - case loaded + case loadedWithoutUpdate + case loaded(_ sections: [ShowDirectorySection]) case error(Error) }