From c7d4ca48b9a1626bf1e7bbf6c9cd279cd7ebb515 Mon Sep 17 00:00:00 2001 From: Sam Wyndham Date: Wed, 27 Nov 2024 09:34:20 +0100 Subject: [PATCH 01/12] Delete `isFolderStatePersistenceEnabled` which is hardcoded to false --- .../ViewModel/ConversationListViewModel.swift | 35 ++----------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift b/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift index 5c2ebc5454..2ac94e9621 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift @@ -252,18 +252,7 @@ final class ConversationListViewModel: NSObject { /// for folder enabled and collapse presistent private lazy var _state: State = { - guard isFolderStatePersistenceEnabled else { return .init() } - - guard let persistentPath = ConversationListViewModel.persistentURL, - let jsonData = try? Data(contentsOf: persistentPath) else { return State() - } - - do { - return try JSONDecoder().decode(ConversationListViewModel.State.self, from: jsonData) - } catch { - log.error("restore state error: \(error)") - return State() - } + return .init() }() private var state: State { @@ -606,10 +595,6 @@ final class ConversationListViewModel: NSObject { // MARK: - state presistent - // TODO: [WPB-7307]: the follow-up PR will remove anything around folders - // https://github.com/wireapp/wire-ios/pull/1466 - let isFolderStatePersistenceEnabled = false - // TODO: [WPB-7307]: remove everything around the legacy folder view (grouped) private struct State: Codable, Equatable { var collapsed: Set @@ -634,23 +619,7 @@ final class ConversationListViewModel: NSObject { } private func saveState(state: State) { - - guard isFolderStatePersistenceEnabled, - let jsonString = state.jsonString, - let persistentDirectory = ConversationListViewModel.persistentDirectory, - let directoryURL = URL.directoryURL(persistentDirectory) else { return } - - try! FileManager.default.createAndProtectDirectory(at: directoryURL) - - do { - try jsonString.write( - to: directoryURL.appendingPathComponent(ConversationListViewModel.persistentFilename), - atomically: true, - encoding: .utf8 - ) - } catch { - log.error("error writing ConversationListViewModel to \(directoryURL): \(error)") - } + return } private static var persistentDirectory: String? { From aea3abf7777fb7ec88d55528f9ce18877ce44089 Mon Sep 17 00:00:00 2001 From: Sam Wyndham Date: Wed, 27 Nov 2024 13:39:47 +0100 Subject: [PATCH 02/12] Remove Folder & Section related code --- .../ConversationListViewModelTests.swift | 15 -- .../ConversationListContentController.swift | 73 --------- .../ViewModel/ConversationListViewModel.swift | 139 +----------------- .../ConversationListViewModelDelegate.swift | 4 - 4 files changed, 5 insertions(+), 226 deletions(-) diff --git a/wire-ios/Wire-iOS Tests/ConversationList/ConversationListViewModel/ConversationListViewModelTests.swift b/wire-ios/Wire-iOS Tests/ConversationList/ConversationListViewModel/ConversationListViewModelTests.swift index 9ae477cd17..07abd75578 100644 --- a/wire-ios/Wire-iOS Tests/ConversationList/ConversationListViewModel/ConversationListViewModelTests.swift +++ b/wire-ios/Wire-iOS Tests/ConversationList/ConversationListViewModel/ConversationListViewModelTests.swift @@ -22,18 +22,10 @@ import XCTest @testable import Wire final class MockConversationListViewModelDelegate: NSObject, ConversationListViewModelDelegate { - func listViewModel(_ model: ConversationListViewModel?, didUpdateSection section: Int) { - // no-op - } - func listViewModel(_ model: ConversationListViewModel?, didUpdateSectionForReload section: Int, animated: Bool) { // no-op } - func listViewModel(_ model: ConversationListViewModel?, didChangeFolderEnabled folderEnabled: Bool) { - // no-op - } - func reload( using stagedChangeset: StagedChangeset, interrupt: ((Changeset) -> Bool)?, @@ -65,7 +57,6 @@ final class ConversationListViewModelTests: XCTestCase { override func setUp() { super.setUp() - removeViewModelState() mockUserSession = UserSessionMock() sut = ConversationListViewModel(userSession: mockUserSession) @@ -89,12 +80,6 @@ final class ConversationListViewModelTests: XCTestCase { super.tearDown() } - func removeViewModelState() { - guard let persistentURL = ConversationListViewModel.persistentURL else { return } - - try? FileManager.default.removeItem(at: persistentURL) - } - // 2 group conversations and 1 contact. First group conversation is mock conversation func fillDummyConversations(mockConversation: ZMConversation) { let info = ConversationDirectoryChangeInfo( diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ConversationListContentController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ConversationListContentController.swift index 136a8eb338..8727e32031 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ConversationListContentController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ConversationListContentController.swift @@ -74,8 +74,6 @@ final class ConversationListContentController: UICollectionViewController { flowLayout.sectionInset = .zero self.listViewModel = .init(userSession: userSession) super.init(collectionViewLayout: flowLayout) - - registerSectionHeader() } @available(*, unavailable) @@ -155,52 +153,6 @@ final class ConversationListContentController: UICollectionViewController { clearsSelectionOnViewWillAppear = false } - // MARK: - section header - - override func collectionView( - _ collectionView: UICollectionView, - viewForSupplementaryElementOfKind kind: String, - at indexPath: IndexPath - ) -> UICollectionReusableView { - - switch kind { - case UICollectionView.elementKindSectionHeader: - let section = indexPath.section - - if let header = collectionView.dequeueReusableSupplementaryView( - ofKind: kind, - withReuseIdentifier: ConversationListHeaderView.reuseIdentifier, - for: indexPath - ) as? ConversationListHeaderView { - header.title = listViewModel.sectionHeaderTitle(sectionIndex: section)?.uppercased() - - header.folderBadge = listViewModel.folderBadge(at: section) - - header.collapsed = listViewModel.collapsed(at: section) - - header.tapHandler = { [weak self] collapsed in - self?.listViewModel.setCollapsed(sectionIndex: section, collapsed: collapsed) - } - - return header - } else { - fatal("Unknown supplementary view for \(kind)") - } - default: - fatal("No supplementary view for \(kind)") - } - } - - private func registerSectionHeader() { - collectionView?.register( - ConversationListHeaderView.self, - forSupplementaryViewOfKind: - UICollectionView.elementKindSectionHeader, - withReuseIdentifier: ConversationListHeaderView.reuseIdentifier - ) - - } - /// ensures that the list selection state matches that of the model. func ensureCurrentSelection() { guard let selectedItem = listViewModel.selectedItem else { return } @@ -383,18 +335,6 @@ final class ConversationListContentController: UICollectionViewController { } extension ConversationListContentController: UICollectionViewDelegateFlowLayout { - func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - referenceSizeForHeaderInSection section: Int - ) -> CGSize { - CGSize( - width: collectionView.bounds.size.width, - height: listViewModel.sectionHeaderVisible(section: section) ? CGFloat.ConversationListSectionHeader - .height : 0 - ) - } - func collectionView( _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, @@ -414,17 +354,6 @@ extension ConversationListContentController: UICollectionViewDelegateFlowLayout extension ConversationListContentController: ConversationListViewModelDelegate { - func listViewModel(_ model: ConversationListViewModel?, didUpdateSection section: Int) { - guard let header = collectionView.supplementaryView( - forElementKind: UICollectionView.elementKindSectionHeader, - at: IndexPath(item: 0, section: section) - ) as? ConversationListHeaderView else { - return - } - - header.folderBadge = listViewModel.folderBadge(at: section) - } - func listViewModel(_ model: ConversationListViewModel?, didSelectItem item: ConversationListItem?) { defer { scrollToMessageOnNextSelection = nil @@ -483,8 +412,6 @@ extension ConversationListContentController: ConversationListViewModelDelegate { } } - func listViewModel(_ model: ConversationListViewModel?, didChangeFolderEnabled folderEnabled: Bool) {} - func reload( using stagedChangeset: StagedChangeset, interrupt: ((Changeset) -> Bool)? = nil, diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift b/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift index 2ac94e9621..3d59643fa1 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift @@ -194,28 +194,7 @@ final class ConversationListViewModel: NSObject { } } - weak var delegate: ConversationListViewModelDelegate? { - didSet { - delegateFolderEnableState(newState: state) - } - } - - // TODO: [WPB-7307]: remove everything regarding folders - var folderEnabled: Bool { - get { - state.folderEnabled - } - - set { - guard newValue != state.folderEnabled else { return } - - state.folderEnabled = newValue - - updateAllSections() - delegate?.listViewModelShouldBeReloaded() - delegateFolderEnableState(newState: state) - } - } + weak var delegate: ConversationListViewModelDelegate? // Local copies of the lists. private var sections: [Section] = [] @@ -250,29 +229,6 @@ final class ConversationListViewModel: NSObject { } } - /// for folder enabled and collapse presistent - private lazy var _state: State = { - return .init() - }() - - private var state: State { - get { - _state - } - - set { - /// simulate willSet - - /// assign - if newValue != _state { - _state = newValue - } - - /// simulate didSet - saveState(state: _state) - } - } - private var conversationDirectoryToken: Any? let userSession: UserSession? @@ -286,10 +242,6 @@ final class ConversationListViewModel: NSObject { updateAllSections() } - private func delegateFolderEnableState(newState: State) { - delegate?.listViewModel(self, didChangeFolderEnabled: folderEnabled) - } - private func setupObservers() { conversationDirectoryToken = userSession?.conversationDirectory.addObserver(self) } @@ -298,20 +250,6 @@ final class ConversationListViewModel: NSObject { kind(of: sectionIndex)?.localizedName } - /// return true if seaction header is visible. - /// For .contactRequests section it is always invisible - /// When folderEnabled == true, returns false - /// - /// - Parameter sectionIndex: section number of collection view - /// - Returns: if the section exists and visible, return true. - func sectionHeaderVisible(section: Int) -> Bool { - guard sections.indices.contains(section), - kind(of: section) != .contactRequests, - folderEnabled else { return false } - - return !sections[section].items.isEmpty - } - private func kind(of sectionIndex: Int) -> Section.Kind? { guard sections.indices.contains(sectionIndex) else { return nil } @@ -438,7 +376,7 @@ final class ConversationListViewModel: NSObject { Section( kind: kind, conversationDirectory: conversationDirectory, - collapsed: state.collapsed.contains(kind.identifier) + collapsed: false // FIXME: Removed collapsed property ) } let filterUseCase = FilterConversationsUseCase(conversationContainers: sections) @@ -464,7 +402,7 @@ final class ConversationListViewModel: NSObject { newValue[sectionNumber].items = newList - // Refresh the section header(since it may be hidden if the sectio is empty) when a section becomes + // Refresh the section header(since it may be hidden if the section is empty) when a section becomes // empty/from empty to non-empty if sections[sectionNumber].items.isEmpty || newList.isEmpty { sections = newValue @@ -487,15 +425,6 @@ final class ConversationListViewModel: NSObject { } }) } - - if let kind, - let sectionNumber = sectionNumber(for: kind) { - delegate?.listViewModel(self, didUpdateSection: sectionNumber) - } else { - sections.indices.forEach { - delegate?.listViewModel(self, didUpdateSection: $0) - } - } } @discardableResult @@ -539,15 +468,10 @@ final class ConversationListViewModel: NSObject { } func collapsed(at sectionIndex: Int) -> Bool { - collapsed(at: sectionIndex, state: state) - } - - private func collapsed(at sectionIndex: Int, state: State) -> Bool { - guard let kind = kind(of: sectionIndex) else { return false } - - return state.collapsed.contains(kind.identifier) + false // FIXME: Remove } + // FIXME: Remove /// set a collpase state of a section /// /// - Parameters: @@ -564,12 +488,6 @@ final class ConversationListViewModel: NSObject { guard self.collapsed(at: sectionIndex) != collapsed else { return } guard let sectionNumber = sectionNumber(for: kind) else { return } - if collapsed { - state.collapsed.insert(kind.identifier) - } else { - state.collapsed.remove(kind.identifier) - } - var newValue = sections newValue[sectionNumber] = Section( kind: kind, @@ -592,53 +510,6 @@ final class ConversationListViewModel: NSObject { delegate?.listViewModel(self, didUpdateSectionForReload: sectionIndex, animated: true) } } - - // MARK: - state presistent - - // TODO: [WPB-7307]: remove everything around the legacy folder view (grouped) - private struct State: Codable, Equatable { - var collapsed: Set - var folderEnabled: Bool - - init() { - self.collapsed = [] - self.folderEnabled = false - } - - var jsonString: String? { - let encoder = JSONEncoder() - encoder.outputFormatting = .sortedKeys - guard let jsonData = try? encoder.encode(self) else { return nil } - - return String(decoding: jsonData, as: UTF8.self) - } - } - - var jsonString: String? { - state.jsonString - } - - private func saveState(state: State) { - return - } - - private static var persistentDirectory: String? { - guard let userID = ZMUser.selfUser()?.remoteIdentifier else { return nil } - - return "UI_state/\(userID)" - } - - private static var persistentFilename: String { - let className = String(describing: self) - return "\(className).json" - } - - static var persistentURL: URL? { - guard let persistentDirectory else { return nil } - - return URL.directoryURL(persistentDirectory)? - .appendingPathComponent(ConversationListViewModel.persistentFilename) - } } // MARK: - ZMUserObserving diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModelDelegate.swift b/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModelDelegate.swift index 2cdc68ea40..a1378e23bb 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModelDelegate.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModelDelegate.swift @@ -34,10 +34,6 @@ protocol ConversationListViewModelDelegate: AnyObject { func listViewModel(_ model: ConversationListViewModel?, didUpdateSectionForReload section: Int, animated: Bool) - func listViewModel(_ model: ConversationListViewModel?, didChangeFolderEnabled folderEnabled: Bool) - - func listViewModel(_ model: ConversationListViewModel?, didUpdateSection section: Int) - func reload( using stagedChangeset: StagedChangeset, interrupt: ((Changeset) -> Bool)?, From 5394264a557eb556796bb50ea3676da66f3737b5 Mon Sep 17 00:00:00 2001 From: Sam Wyndham Date: Wed, 27 Nov 2024 14:27:25 +0100 Subject: [PATCH 03/12] Fix mutability of selectedFilterImage --- .../ConversationListViewController+NavigationBar.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/Container/ConversationListViewController+NavigationBar/ConversationListViewController+NavigationBar.swift b/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/Container/ConversationListViewController+NavigationBar/ConversationListViewController+NavigationBar.swift index c744daf1ed..1a51e7514c 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/Container/ConversationListViewController+NavigationBar/ConversationListViewController+NavigationBar.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/Container/ConversationListViewController+NavigationBar/ConversationListViewController+NavigationBar.swift @@ -181,7 +181,7 @@ extension ConversationListViewController: ConversationListContainerViewModelDele withConfiguration: symbolConfiguration )! - var selectedFilterImage: UIImage = switch listContentController.listViewModel.selectedFilter { + let selectedFilterImage: UIImage = switch listContentController.listViewModel.selectedFilter { case .favorites, .groups, .oneOnOne, .folder: filledFilterImage case .none: From a47dccde4fdca3ccf6766a19073bb92c41c5498a Mon Sep 17 00:00:00 2001 From: Sam Wyndham Date: Wed, 27 Nov 2024 17:04:39 +0100 Subject: [PATCH 04/12] Replace `ConversationDirectoryType.allFolders` with `ConversationDirectoryType.nonDeletedFolders` --- .../Source/ConversationList/ConversationDirectory.swift | 4 ++-- .../Source/ConversationList/ZMConversationListDirectory.h | 1 + .../Source/ConversationList/ZMConversationListDirectory.m | 6 ++++++ wire-ios/Tests/Mocks/MockConverationDirectory.swift | 2 +- .../ViewModel/ConversationListViewModel.swift | 2 +- .../Folders/FolderPickerViewControllerBuilder.swift | 2 +- .../Folders/Mappers/WireFolderDirectoryMapper.swift | 2 +- 7 files changed, 13 insertions(+), 6 deletions(-) diff --git a/wire-ios-data-model/Source/ConversationList/ConversationDirectory.swift b/wire-ios-data-model/Source/ConversationList/ConversationDirectory.swift index d522f010ce..6ea4918e65 100644 --- a/wire-ios-data-model/Source/ConversationList/ConversationDirectory.swift +++ b/wire-ios-data-model/Source/ConversationList/ConversationDirectory.swift @@ -50,8 +50,8 @@ public protocol ConversationDirectoryObserver: AnyObject { public protocol ConversationDirectoryType { - /// All folder created by the user - var allFolders: [LabelType] { get } + /// Folders excluding those marked for deletion + var nonDeletedFolders: [LabelType] { get } /// Create a new folder with a given name func createFolder(_ name: String) -> LabelType? diff --git a/wire-ios-data-model/Source/ConversationList/ZMConversationListDirectory.h b/wire-ios-data-model/Source/ConversationList/ZMConversationListDirectory.h index e332cb5c5f..8dfd20fc4c 100644 --- a/wire-ios-data-model/Source/ConversationList/ZMConversationListDirectory.h +++ b/wire-ios-data-model/Source/ConversationList/ZMConversationListDirectory.h @@ -42,6 +42,7 @@ @property (nonatomic, readonly, nonnull) NSMutableDictionary *listsByFolder; @property (nonatomic, readonly, nonnull) NSArray> *allFolders; +@property (nonatomic, readonly, nonnull) NSArray> *nonDeletedFolders; /// Refetches all conversation lists and resets the snapshots diff --git a/wire-ios-data-model/Source/ConversationList/ZMConversationListDirectory.m b/wire-ios-data-model/Source/ConversationList/ZMConversationListDirectory.m index 942d0706b4..fffee72867 100644 --- a/wire-ios-data-model/Source/ConversationList/ZMConversationListDirectory.m +++ b/wire-ios-data-model/Source/ConversationList/ZMConversationListDirectory.m @@ -203,6 +203,12 @@ - (void)refetchAllListsInManagedObjectContext:(NSManagedObjectContext *)moc return self.folderList.backingList; } +- (NSArray> *)nonDeletedFolders +{ + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"markedForDeletion == NO"]; + return [self.folderList.backingList filteredArrayUsingPredicate:predicate]; +} + @end diff --git a/wire-ios/Tests/Mocks/MockConverationDirectory.swift b/wire-ios/Tests/Mocks/MockConverationDirectory.swift index e465b896d1..dfec409af2 100644 --- a/wire-ios/Tests/Mocks/MockConverationDirectory.swift +++ b/wire-ios/Tests/Mocks/MockConverationDirectory.swift @@ -21,7 +21,7 @@ import WireDataModel class MockConversationDirectory: ConversationDirectoryType { - var allFolders: [LabelType] = [] + var nonDeletedFolders: [LabelType] = [] var mockGroupConversations: [ZMConversation] = [] var mockContactsConversations: [ZMConversation] = [] var mockFavoritesConversations: [ZMConversation] = [] diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift b/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift index 3d59643fa1..2942d1c67a 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift @@ -362,7 +362,7 @@ final class ConversationListViewModel: NSObject { case .oneOnOne: [.contacts, .contactRequests] case let .folder(id, _): - if let folder = conversationDirectory.allFolders.first(where: { $0.remoteIdentifier == id }) { + if let folder = conversationDirectory.nonDeletedFolders.first(where: { $0.remoteIdentifier == id }) { [.folder(label: folder)] } else { // FIXME: [WPB-13905] Log invalid state once WPB-13905 is implemented diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Folders/FolderPickerViewControllerBuilder.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Folders/FolderPickerViewControllerBuilder.swift index 1174256f11..6b2e3ca45a 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Folders/FolderPickerViewControllerBuilder.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Folders/FolderPickerViewControllerBuilder.swift @@ -31,7 +31,7 @@ struct FolderPickerViewControllerBuilder { @MainActor func build(mainCoordinator: AnyMainCoordinator, showCloseButton: Bool) -> UIViewController { - let folders: [FolderPickerOption] = conversationDirectory.allFolders.compactMap { + let folders: [FolderPickerOption] = conversationDirectory.nonDeletedFolders.compactMap { guard let id = $0.remoteIdentifier, let title = $0.name else { return nil } return FolderPickerOption(id: id, title: title) diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Folders/Mappers/WireFolderDirectoryMapper.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Folders/Mappers/WireFolderDirectoryMapper.swift index cc80f2eb8c..4ed26ef515 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Folders/Mappers/WireFolderDirectoryMapper.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Folders/Mappers/WireFolderDirectoryMapper.swift @@ -27,7 +27,7 @@ struct WireFolderDirectoryMapper: FolderDirectoryTypeProtocol { } var allFolders: [Folder] { - directory.allFolders.map { label -> Folder in + directory.nonDeletedFolders.map { label -> Folder in Folder(identifier: label.remoteIdentifier, name: label.name ?? "") } } From 12b6b3aab3754019d17cd65fb92703fc0b9324e2 Mon Sep 17 00:00:00 2001 From: Sam Wyndham Date: Thu, 28 Nov 2024 10:08:05 +0100 Subject: [PATCH 05/12] Pass ConversationDirectoryType to ConversationDirectoryObserver methods --- .../ConversationDirectory.swift | 36 ++++++++++--------- .../ConversationListViewModelTests.swift | 5 ++- .../ViewModel/ConversationListViewModel.swift | 5 ++- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/wire-ios-data-model/Source/ConversationList/ConversationDirectory.swift b/wire-ios-data-model/Source/ConversationList/ConversationDirectory.swift index 6ea4918e65..014083f175 100644 --- a/wire-ios-data-model/Source/ConversationList/ConversationDirectory.swift +++ b/wire-ios-data-model/Source/ConversationList/ConversationDirectory.swift @@ -44,7 +44,10 @@ public struct ConversationDirectoryChangeInfo { public protocol ConversationDirectoryObserver: AnyObject { - func conversationDirectoryDidChange(_ changeInfo: ConversationDirectoryChangeInfo) + func conversationDirectoryDidChange( + conversationDirectory: ConversationDirectoryType, + changeInfo: ConversationDirectoryChangeInfo + ) } @@ -136,19 +139,17 @@ private class ConversationListObserverProxy: NSObject, ZMConversationListObserve } func conversationListsDidReload() { - observer?.conversationDirectoryDidChange(ConversationDirectoryChangeInfo( - reloaded: true, - updatedLists: [], - updatedFolders: false - )) + observer?.conversationDirectoryDidChange( + conversationDirectory: directory, + changeInfo: ConversationDirectoryChangeInfo(reloaded: true, updatedLists: [], updatedFolders: false) + ) } func conversationListsDidChangeFolders() { - observer?.conversationDirectoryDidChange(ConversationDirectoryChangeInfo( - reloaded: false, - updatedLists: [], - updatedFolders: true - )) + observer?.conversationDirectoryDidChange( + conversationDirectory: directory, + changeInfo: ConversationDirectoryChangeInfo(reloaded: false, updatedLists: [], updatedFolders: true) + ) } func conversationListDidChange(_ changeInfo: ConversationListChangeInfo) { @@ -170,11 +171,14 @@ private class ConversationListObserverProxy: NSObject, ZMConversationListObserve [] } - observer?.conversationDirectoryDidChange(ConversationDirectoryChangeInfo( - reloaded: false, - updatedLists: updatedLists, - updatedFolders: false - )) + observer?.conversationDirectoryDidChange( + conversationDirectory: directory, + changeInfo: ConversationDirectoryChangeInfo( + reloaded: false, + updatedLists: updatedLists, + updatedFolders: false + ) + ) } } diff --git a/wire-ios/Wire-iOS Tests/ConversationList/ConversationListViewModel/ConversationListViewModelTests.swift b/wire-ios/Wire-iOS Tests/ConversationList/ConversationListViewModel/ConversationListViewModelTests.swift index 07abd75578..0013ae7e41 100644 --- a/wire-ios/Wire-iOS Tests/ConversationList/ConversationListViewModel/ConversationListViewModelTests.swift +++ b/wire-ios/Wire-iOS Tests/ConversationList/ConversationListViewModel/ConversationListViewModelTests.swift @@ -100,7 +100,10 @@ final class ConversationListViewModelTests: XCTestCase { mockUserSession.mockConversationDirectory.mockGroupConversations = [mockConversation, teamConversation] mockUserSession.mockConversationDirectory.mockContactsConversations = [oneToOneConversation] - sut.conversationDirectoryDidChange(info) + sut.conversationDirectoryDidChange( + conversationDirectory: mockUserSession.mockConversationDirectory, + changeInfo: info + ) } func testForNumberOfItems() { diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift b/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift index 2942d1c67a..71aaff8757 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift @@ -519,7 +519,10 @@ private let log = ZMSLog(tag: "ConversationListViewModel") // MARK: - ConversationDirectoryObserver extension ConversationListViewModel: ConversationDirectoryObserver { - func conversationDirectoryDidChange(_ changeInfo: ConversationDirectoryChangeInfo) { + func conversationDirectoryDidChange( + conversationDirectory: ConversationDirectoryType, + changeInfo: ConversationDirectoryChangeInfo + ) { if changeInfo.reloaded { // If the section was empty in certain cases collection view breaks down on the big amount of conversations, From 615b46c47d80d3423fdc9f381cb60b0653837bc6 Mon Sep 17 00:00:00 2001 From: Sam Wyndham Date: Thu, 28 Nov 2024 10:09:10 +0100 Subject: [PATCH 06/12] Introduce `MainCoordinator.applyConversationFilter(_:)` We need a method to apply the filter with doing any navigation --- .../Coordinator/AnyMainCoordinator.swift | 9 +++++++++ .../Coordinator/MainCoordinator.swift | 4 ++++ .../Protocols/Coordinator/MainCoordinatorProtocol.swift | 2 ++ .../MainCoordinator/MockMainCoordinator.swift | 5 +++++ 4 files changed, 20 insertions(+) diff --git a/WireUI/Sources/WireMainNavigationUI/Coordinator/AnyMainCoordinator.swift b/WireUI/Sources/WireMainNavigationUI/Coordinator/AnyMainCoordinator.swift index 278b6b3d97..53333a80e6 100644 --- a/WireUI/Sources/WireMainNavigationUI/Coordinator/AnyMainCoordinator.swift +++ b/WireUI/Sources/WireMainNavigationUI/Coordinator/AnyMainCoordinator.swift @@ -24,6 +24,7 @@ public final class AnyMainCoordinator Void + private let _applyConversationFilter: @MainActor (_ conversationFilter: ConversationFilter?) -> Void private let _showArchive: @MainActor () async -> Void private let _showSettings: @MainActor () async -> Void private let _showConversation: @MainActor ( @@ -44,6 +45,9 @@ public final class AnyMainCoordinator: NSObject, MainCoordinatorProto } } + applyConversationFilter(conversationFilter) + } + + public func applyConversationFilter(_ conversationFilter: ConversationFilter?) { // apply the filter to the conversation list let conversationFilter = conversationFilter.map { ConversationFilter(mappingFrom: $0) } conversationListUI.conversationFilter = conversationFilter diff --git a/WireUI/Sources/WireMainNavigationUI/Protocols/Coordinator/MainCoordinatorProtocol.swift b/WireUI/Sources/WireMainNavigationUI/Protocols/Coordinator/MainCoordinatorProtocol.swift index 8b9593d9f7..f6110d7a5b 100644 --- a/WireUI/Sources/WireMainNavigationUI/Protocols/Coordinator/MainCoordinatorProtocol.swift +++ b/WireUI/Sources/WireMainNavigationUI/Protocols/Coordinator/MainCoordinatorProtocol.swift @@ -30,6 +30,8 @@ public protocol MainCoordinatorProtocol: AnyObject { @MainActor func showConversationList(conversationFilter: ConversationFilter?) async @MainActor + func applyConversationFilter(_ filter: ConversationFilter?) + @MainActor func showArchive() async @MainActor func showSettings() async diff --git a/wire-ios/Wire-iOS Tests/UserInterface/MainCoordinator/MockMainCoordinator.swift b/wire-ios/Wire-iOS Tests/UserInterface/MainCoordinator/MockMainCoordinator.swift index 72f69ab357..218f65f79a 100644 --- a/wire-ios/Wire-iOS Tests/UserInterface/MainCoordinator/MockMainCoordinator.swift +++ b/wire-ios/Wire-iOS Tests/UserInterface/MainCoordinator/MockMainCoordinator.swift @@ -32,6 +32,11 @@ final class MockMainCoordinator: MainCoordinatorProtocol { fatalError("Mock method not implemented") } + @MainActor + func applyConversationFilter(_ filter: ConversationFilter?) { + fatalError("Mock method not implemented") + } + @MainActor func showArchive() { fatalError("Mock method not implemented") From 5e8874ed92672b761c87c8d37bd16acdd785d54e Mon Sep 17 00:00:00 2001 From: Sam Wyndham Date: Thu, 28 Nov 2024 13:56:14 +0100 Subject: [PATCH 07/12] Introduce `ConversationFilterSelector` --- wire-ios/Wire-iOS.xcodeproj/project.pbxproj | 4 ++ .../ConversationFilterSelector.swift | 59 +++++++++++++++++++ .../ZClientViewController.swift | 9 +++ 3 files changed, 72 insertions(+) create mode 100644 wire-ios/Wire-iOS/Sources/UserInterface/MainController/ConversationFilterSelector.swift diff --git a/wire-ios/Wire-iOS.xcodeproj/project.pbxproj b/wire-ios/Wire-iOS.xcodeproj/project.pbxproj index a38acfd016..da7086b859 100644 --- a/wire-ios/Wire-iOS.xcodeproj/project.pbxproj +++ b/wire-ios/Wire-iOS.xcodeproj/project.pbxproj @@ -1164,6 +1164,7 @@ CB43DBC42CE639E800BF5AEB /* FolderPickerViewControllerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB43DBC32CE639E800BF5AEB /* FolderPickerViewControllerBuilder.swift */; }; CB4870F22C7F4FE5001E9151 /* WireTransportSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB4870F12C7F4FE5001E9151 /* WireTransportSupport.framework */; }; CB4E15122C81CC81005DDEC8 /* Down in Frameworks */ = {isa = PBXBuildFile; productRef = CB4E15112C81CC81005DDEC8 /* Down */; }; + CB8E51E32CF89D9B00F7F01D /* ConversationFilterSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB8E51E22CF89D9B00F7F01D /* ConversationFilterSelector.swift */; }; D30880FB292CD8F200DDEAB0 /* CallingBottomSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D30880FA292CD8F200DDEAB0 /* CallingBottomSheetViewController.swift */; }; D30880FE292E521D00DDEAB0 /* CallingActionsInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D30880FD292E521D00DDEAB0 /* CallingActionsInfoViewController.swift */; }; D3095761283CEB1C00CEC620 /* MediaShareRestrictionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D309575D283CDFED00CEC620 /* MediaShareRestrictionManager.swift */; }; @@ -3243,6 +3244,7 @@ CB366A8F2CC7DE410083701F /* ArchivedListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchivedListViewModelTests.swift; sourceTree = ""; }; CB43DBC32CE639E800BF5AEB /* FolderPickerViewControllerBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderPickerViewControllerBuilder.swift; sourceTree = ""; }; CB4870F12C7F4FE5001E9151 /* WireTransportSupport.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = WireTransportSupport.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + CB8E51E22CF89D9B00F7F01D /* ConversationFilterSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationFilterSelector.swift; sourceTree = ""; }; CE06C93E1DF5C3D900497685 /* AVAsset+VideoConvert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVAsset+VideoConvert.swift"; sourceTree = ""; }; CE8E4FA91DF066750009F437 /* FileMetaDataGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileMetaDataGenerator.swift; sourceTree = ""; }; CE8E4FB21DF066EE0009F437 /* AudioProcessing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioProcessing.swift; sourceTree = ""; }; @@ -6750,6 +6752,7 @@ children = ( 59AF77A02CC7FB39002438D1 /* AnyMainCoordinator.swift */, 596184AC2CC7B5F600787AF0 /* DefaultSettingsPropertyFactoryDelegate.swift */, + CB8E51E22CF89D9B00F7F01D /* ConversationFilterSelector.swift */, 5965E9722C9B1491001D8AE1 /* ConversationListViewController+MainConversationListProtocol.swift */, 59E67CB12CA8C356000F1C17 /* ConversationRootViewController+MainConversationProtocol.swift */, 597CAEB22CA40111002A1160 /* MainCoordinator+ArchivedListViewControllerDelegate.swift */, @@ -10191,6 +10194,7 @@ 5E65A7A721304B6B008BFCC0 /* UserEmailUpdateCodeSentEventHandler.swift in Sources */, EF3371C22216D9D9005ED048 /* ZMUser+Self.swift in Sources */, EFEE97C823229CF5007A4702 /* ConversationListCellDelegate.swift in Sources */, + CB8E51E32CF89D9B00F7F01D /* ConversationFilterSelector.swift in Sources */, 7CED30721FD97748009F0DAC /* IconStringsBuilder.swift in Sources */, E9B0DED42B5E7151006DC9E4 /* AccentColorPicker.swift in Sources */, EFABC92420208D80001F9866 /* UIViewController+removeUserConfirmationUI.swift in Sources */, diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ConversationFilterSelector.swift b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ConversationFilterSelector.swift new file mode 100644 index 0000000000..8ba0853f0d --- /dev/null +++ b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ConversationFilterSelector.swift @@ -0,0 +1,59 @@ +// +// Wire +// Copyright (C) 2024 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import WireDataModel + +/// Updates `conversationFilter` based on system events rather than user selection. +/// +/// For example, if the current filter is a folder filter, and the folder is deleted, this selector will remove +/// the filter. + +@MainActor +final class ConversationFilterSelector: @preconcurrency ConversationDirectoryObserver { + + private let mainCoordinator: AnyMainCoordinator + private let conversationFilter: () -> ConversationFilter? + private var observation: Any? + + /// Creates a new ConversationFilterSelector. + /// + /// - Parameters: + /// - mainCoordinator: The main coordinator. + /// - conversationFilter: A closure that returns the current conversation filter. + + init(mainCoordinator: AnyMainCoordinator, conversationFilter: @escaping () -> ConversationFilter?) { + self.mainCoordinator = mainCoordinator + self.conversationFilter = conversationFilter + } + + /// Start observing `conversationDirectory`. + + func observe(conversationDirectory: any ConversationDirectoryType) { + observation = conversationDirectory.addObserver(self) + } + + func conversationDirectoryDidChange( + conversationDirectory: ConversationDirectoryType, + changeInfo: ConversationDirectoryChangeInfo + ) { + if case let .folder(id, _) = conversationFilter(), + conversationDirectory.nonDeletedFolders.first(where: { $0.remoteIdentifier == id }) == nil { + mainCoordinator.applyConversationFilter(.none) + } + } +} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController.swift index 7c9fbfbe75..33f680dfc7 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController.swift @@ -46,6 +46,13 @@ final class ZClientViewController: UIViewController { private(set) var conversationRootViewController: UIViewController? + private lazy var conversationFilterSelector = ConversationFilterSelector( + mainCoordinator: AnyMainCoordinator(mainCoordinator: mainCoordinator), + conversationFilter: { [weak conversationListViewController] in + conversationListViewController?.conversationFilter + } + ) + var currentConversation: ZMConversation? { conversationListViewController.selectedConversation } @@ -320,6 +327,8 @@ final class ZClientViewController: UIViewController { await updateCachedAccountImage() await updateCachedAccountInfo() } + + conversationFilterSelector.observe(conversationDirectory: userSession.conversationDirectory) } override var supportedInterfaceOrientations: UIInterfaceOrientationMask { From d2a326d868d115c5e6242f1e3cc91ec64be67b49 Mon Sep 17 00:00:00 2001 From: Sam Wyndham Date: Thu, 28 Nov 2024 14:16:55 +0100 Subject: [PATCH 08/12] Remove code related to collapsing sections --- .../ViewModel/ConversationListViewModel.swift | 78 ++----------------- 1 file changed, 5 insertions(+), 73 deletions(-) diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift b/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift index 71aaff8757..76f3253cf7 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift @@ -142,10 +142,9 @@ final class ConversationListViewModel: NSObject { var kind: Kind var items: [SectionItem] - var collapsed: Bool var elements: [SectionItem] { - collapsed ? [] : items + items } /// ref to AggregateArray, we return the first found item's index @@ -166,33 +165,22 @@ final class ConversationListViewModel: NSObject { init(source: ConversationListViewModel.Section, elements: some Collection) { self.kind = source.kind - self.collapsed = source.collapsed self.items = Array(elements) } init( kind: Kind, - conversationDirectory: ConversationDirectoryType, - collapsed: Bool + conversationDirectory: ConversationDirectoryType ) { self.items = ConversationListViewModel.newList(for: kind, conversationDirectory: conversationDirectory) self.kind = kind - self.collapsed = collapsed } } static let contactRequestsItem: ConversationListConnectRequestsItem = .init() /// current selected ZMConversaton or ConversationListConnectRequestsItem object - private(set) var selectedItem: ConversationListItem? { - didSet { - /// expand the section if selcted item is update - guard let indexPath = indexPath(for: selectedItem), - collapsed(at: indexPath.section) else { return } - - setCollapsed(sectionIndex: indexPath.section, collapsed: false, batchUpdate: false) - } - } + private(set) var selectedItem: ConversationListItem? weak var delegate: ConversationListViewModelDelegate? @@ -273,8 +261,7 @@ final class ConversationListViewModel: NSObject { } func numberOfItems(inSection sectionIndex: Int) -> Int { - guard sections.indices.contains(sectionIndex), - !collapsed(at: sectionIndex) else { return 0 } + guard sections.indices.contains(sectionIndex) else { return 0 } return sections[sectionIndex].elements.count } @@ -375,8 +362,7 @@ final class ConversationListViewModel: NSObject { let sections = kinds.map { kind in Section( kind: kind, - conversationDirectory: conversationDirectory, - collapsed: false // FIXME: Removed collapsed property + conversationDirectory: conversationDirectory ) } let filterUseCase = FilterConversationsUseCase(conversationContainers: sections) @@ -456,60 +442,6 @@ final class ConversationListViewModel: NSObject { delegate?.listViewModel(self, didSelectItem: itemToSelect) } } - - // MARK: - folder badge - - func folderBadge(at sectionIndex: Int) -> Int { - sections[sectionIndex].items.filter { - let status = ($0.item as? ZMConversation)?.status - return status?.messagesRequiringAttention.isEmpty == false && - status?.showingAllMessages == true - }.count - } - - func collapsed(at sectionIndex: Int) -> Bool { - false // FIXME: Remove - } - - // FIXME: Remove - /// set a collpase state of a section - /// - /// - Parameters: - /// - sectionIndex: section to update - /// - collapsed: collapsed or expanded - /// - batchUpdate: true for update with difference kit comparison, false for reload the section animated - func setCollapsed( - sectionIndex: Int, - collapsed: Bool, - batchUpdate: Bool = true - ) { - guard let conversationDirectory = userSession?.conversationDirectory else { return } - guard let kind = kind(of: sectionIndex) else { return } - guard self.collapsed(at: sectionIndex) != collapsed else { return } - guard let sectionNumber = sectionNumber(for: kind) else { return } - - var newValue = sections - newValue[sectionNumber] = Section( - kind: kind, - conversationDirectory: conversationDirectory, - collapsed: collapsed - ) - - if batchUpdate { - let changeset = StagedChangeset(source: sections, target: newValue) - - delegate?.reload(using: changeset, interrupt: { _ in - false - }, setData: { data in - if let data { - self.sections = data - } - }) - } else { - sections = newValue - delegate?.listViewModel(self, didUpdateSectionForReload: sectionIndex, animated: true) - } - } } // MARK: - ZMUserObserving From ffdda00d7643448866237d4552f063246d8b5302 Mon Sep 17 00:00:00 2001 From: Sam Wyndham Date: Thu, 28 Nov 2024 14:30:06 +0100 Subject: [PATCH 09/12] Remove `mainCoordinator` as a dependency from `ConversationFilterSelector` --- .../MainController/ConversationFilterSelector.swift | 11 +++++++---- .../MainController/ZClientViewController.swift | 4 +++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ConversationFilterSelector.swift b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ConversationFilterSelector.swift index 8ba0853f0d..a1d36194e4 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ConversationFilterSelector.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ConversationFilterSelector.swift @@ -26,8 +26,8 @@ import WireDataModel @MainActor final class ConversationFilterSelector: @preconcurrency ConversationDirectoryObserver { - private let mainCoordinator: AnyMainCoordinator private let conversationFilter: () -> ConversationFilter? + private let updateConversationFilter: (ConversationFilter?) -> Void private var observation: Any? /// Creates a new ConversationFilterSelector. @@ -36,9 +36,12 @@ final class ConversationFilterSelector: @preconcurrency ConversationDirectoryObs /// - mainCoordinator: The main coordinator. /// - conversationFilter: A closure that returns the current conversation filter. - init(mainCoordinator: AnyMainCoordinator, conversationFilter: @escaping () -> ConversationFilter?) { - self.mainCoordinator = mainCoordinator + init( + conversationFilter: @escaping () -> ConversationFilter?, + updateConversationFilter: @escaping (ConversationFilter?) -> Void + ) { self.conversationFilter = conversationFilter + self.updateConversationFilter = updateConversationFilter } /// Start observing `conversationDirectory`. @@ -53,7 +56,7 @@ final class ConversationFilterSelector: @preconcurrency ConversationDirectoryObs ) { if case let .folder(id, _) = conversationFilter(), conversationDirectory.nonDeletedFolders.first(where: { $0.remoteIdentifier == id }) == nil { - mainCoordinator.applyConversationFilter(.none) + updateConversationFilter(nil) } } } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController.swift index 33f680dfc7..69e39eb46a 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController.swift @@ -47,9 +47,11 @@ final class ZClientViewController: UIViewController { private(set) var conversationRootViewController: UIViewController? private lazy var conversationFilterSelector = ConversationFilterSelector( - mainCoordinator: AnyMainCoordinator(mainCoordinator: mainCoordinator), conversationFilter: { [weak conversationListViewController] in conversationListViewController?.conversationFilter + }, + updateConversationFilter: { [weak mainCoordinator] filter in + mainCoordinator?.applyConversationFilter(filter) } ) From 22e9fd646782057cc30f213bbaa7158ff889ee09 Mon Sep 17 00:00:00 2001 From: Sam Wyndham Date: Fri, 29 Nov 2024 11:35:16 +0100 Subject: [PATCH 10/12] Test `ConversationFilterSelector` --- .../ConversationFilterSelectorTests.swift | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 wire-ios/Wire-iOS Tests/UserInterface/MainController/ConversationFilterSelectorTests.swift diff --git a/wire-ios/Wire-iOS Tests/UserInterface/MainController/ConversationFilterSelectorTests.swift b/wire-ios/Wire-iOS Tests/UserInterface/MainController/ConversationFilterSelectorTests.swift new file mode 100644 index 0000000000..c573b9afe1 --- /dev/null +++ b/wire-ios/Wire-iOS Tests/UserInterface/MainController/ConversationFilterSelectorTests.swift @@ -0,0 +1,105 @@ +// +// Wire +// Copyright (C) 2024 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import WireDataModel +import XCTest + +@testable import Wire + +final class ConversationFilterSelectorTests: XCTestCase { + + private var sut: ConversationFilterSelector! + private var conversationDirectory: MockConversationDirectory! + private var conversationFilter: ConversationFilter? + private var conversationFilterDidChange: Bool! + + @MainActor + override func setUpWithError() throws { + conversationDirectory = MockConversationDirectory() + sut = ConversationFilterSelector( + conversationFilter: { [unowned self] in conversationFilter }, + updateConversationFilter: { [unowned self] newValue in + conversationFilter = newValue + conversationFilterDidChange = true + } + ) + conversationFilterDidChange = false + } + + @MainActor + override func tearDownWithError() throws { + conversationDirectory = nil + sut = nil + conversationFilterDidChange = nil + } + + @MainActor + func testConversationDirectoryDidChange_whenFolderFilterSelected() throws { + // GIVEN + let folderA = MockLabel(remoteIdentifier: UUID()) + let folderB = MockLabel(remoteIdentifier: UUID()) + + conversationFilter = .folder(id: folderA.remoteIdentifier!, name: "folderName") + + // WHEN + conversationDirectory.nonDeletedFolders = [folderA, folderB] + sut.conversationDirectoryDidChange(conversationDirectory: conversationDirectory, changeInfo: .someValue) + + // THEN + XCTAssertFalse(conversationFilterDidChange) + XCTAssertEqual(conversationFilter, .folder(id: folderA.remoteIdentifier!, name: "folderName")) + + // WHEN + conversationDirectory.nonDeletedFolders = [folderB] + sut.conversationDirectoryDidChange(conversationDirectory: conversationDirectory, changeInfo: .someValue) + + // THEN + XCTAssertTrue(conversationFilterDidChange) + XCTAssertNil(conversationFilter) + } + + @MainActor + func testConversationDirectoryDidChange_whenNotFolderFilterSelected() throws { + let testCases: [ConversationFilter?] = [ + .favorites, + .groups, + .oneOnOne, + .none + ] + + for testCase in testCases { + // GIVEN + conversationFilter = testCase + + // WHEN + sut.conversationDirectoryDidChange(conversationDirectory: conversationDirectory, changeInfo: .someValue) + + // THEN + XCTAssertFalse(conversationFilterDidChange) + } + } + +} + +private extension ConversationDirectoryChangeInfo { + static let someValue = ConversationDirectoryChangeInfo( + reloaded: false, + updatedLists: [], + updatedFolders: false + ) +} From 2f8368e8df12790d754b52fe488c96906b84ae05 Mon Sep 17 00:00:00 2001 From: Sam Wyndham Date: Fri, 29 Nov 2024 14:31:41 +0100 Subject: [PATCH 11/12] Empty commit From 24b1f724a99043a79d44b44bfbd550325d90b571 Mon Sep 17 00:00:00 2001 From: Sam Wyndham Date: Fri, 29 Nov 2024 15:02:16 +0100 Subject: [PATCH 12/12] Test `AnyMainCoordinator.applyConversationFilter` --- .../Coordinator/AnyMainCoordinatorTests.swift | 9 +++++++++ .../Mocks/MockMainCoordinator.swift | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/WireUI/Tests/WireMainNavigationUITests/Coordinator/AnyMainCoordinatorTests.swift b/WireUI/Tests/WireMainNavigationUITests/Coordinator/AnyMainCoordinatorTests.swift index 5b5cf7042f..3dc55e89be 100644 --- a/WireUI/Tests/WireMainNavigationUITests/Coordinator/AnyMainCoordinatorTests.swift +++ b/WireUI/Tests/WireMainNavigationUITests/Coordinator/AnyMainCoordinatorTests.swift @@ -45,6 +45,15 @@ final class AnyMainCoordinatorTests: XCTestCase { XCTAssertEqual(mockMainCoordinator.showConversationList_Invocations.first, .groups) } + func testApplyFilterIsInvoked() { + // When + sut.applyConversationFilter(.groups) + + // Then + XCTAssertEqual(mockMainCoordinator.applyConversationFilter_Invocations.count, 1) + XCTAssertEqual(mockMainCoordinator.applyConversationFilter_Invocations.first, .groups) + } + func testShowArchiveIsInvoked() async { // When await sut.showArchive() diff --git a/WireUI/Tests/WireMainNavigationUITests/Mocks/MockMainCoordinator.swift b/WireUI/Tests/WireMainNavigationUITests/Mocks/MockMainCoordinator.swift index 9fea64dd3f..be921a0975 100644 --- a/WireUI/Tests/WireMainNavigationUITests/Mocks/MockMainCoordinator.swift +++ b/WireUI/Tests/WireMainNavigationUITests/Mocks/MockMainCoordinator.swift @@ -29,6 +29,11 @@ final class MockMainCoordinatorProtocol: MainCoordinatorProtocol { showConversationList_Invocations += [conversationFilter] } + var applyConversationFilter_Invocations: [ConversationFilter?] = [] + func applyConversationFilter(_ filter: ConversationFilter?) { + applyConversationFilter_Invocations += [filter] + } + var showArchive_Invocations: [Void] = [] func showArchive() async { showArchive_Invocations.append(())