From fc0f7a72a96be1d531194ee643d67d711cb30702 Mon Sep 17 00:00:00 2001 From: manuroe Date: Tue, 12 Oct 2021 13:41:53 +0200 Subject: [PATCH 01/23] Prepare for new sprint --- Config/AppVersion.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 5e3a09e8f..79d20fa80 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.6.4 -CURRENT_PROJECT_VERSION = 1.6.4 +MARKETING_VERSION = 1.6.5 +CURRENT_PROJECT_VERSION = 1.6.5 From 8f83db97cb19d89489e6bbb79ef00eed870049c6 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 28 Sep 2021 14:28:28 +0300 Subject: [PATCH 02/23] vector-im/element-ios/issues/1098 - Fixed typo in template file name. --- ...aters.swift => TemplateUserProfileCoordinatorParameters.swift} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/{TemplateUserProfileCoordinatorParamaters.swift => TemplateUserProfileCoordinatorParameters.swift} (100%) diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinatorParamaters.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinatorParameters.swift similarity index 100% rename from RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinatorParamaters.swift rename to RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinatorParameters.swift From 5008d4f810fb7521c6085795502e0524cd418f8d Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 29 Sep 2021 11:55:05 +0300 Subject: [PATCH 03/23] vector-im/element-ios/issues/1098 - Fixed indentation in templates. Added UnitTests to the main RiotSwiftUI target. --- .../Coordinator/TemplateUserProfileCoordinator.swift | 2 +- .../TemplateUserProfileCoordinatorParameters.swift | 2 +- RiotSwiftUI/target.yml | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift index 6707bd839..cdd0e48cb 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift @@ -55,7 +55,7 @@ final class TemplateUserProfileCoordinator: Coordinator { switch result { case .cancel, .done: self.completion?() - break + break } } } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinatorParameters.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinatorParameters.swift index 17be5c41e..7f162ce38 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinatorParameters.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinatorParameters.swift @@ -17,5 +17,5 @@ import Foundation struct TemplateUserProfileCoordinatorParameters { - let session: MXSession + let session: MXSession } diff --git a/RiotSwiftUI/target.yml b/RiotSwiftUI/target.yml index 5200f73ca..ecbf8a91c 100644 --- a/RiotSwiftUI/target.yml +++ b/RiotSwiftUI/target.yml @@ -10,6 +10,7 @@ schemes: targets: RiotSwiftUI: - running + - testing - profiling - analyzing - archiving @@ -18,6 +19,11 @@ schemes: run: config: Debug disableMainThreadChecker: true + test: + config: Debug + disableMainThreadChecker: true + targets: + - RiotSwiftUnitTests targets: RiotSwiftUI: From ee9553fee536594effe823773bf07fa5ad0fe838 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 30 Sep 2021 09:37:59 +0300 Subject: [PATCH 04/23] #1098 - Generated UserSuggestion from template, got initial UI working and automatically updating. --- Riot/Modules/Room/RoomViewController.m | 5 + .../Views/InputToolbar/RoomInputToolbarView.m | 6 +- .../Modules/Common/Mock/MockAppScreens.swift | 3 +- .../Common/Util/RoundedCornerShape.swift | 30 ++++++ .../ViewModel/StateStoreViewModel.swift | 1 - .../UserSuggestionCoordinator.swift | 73 +++++++++++++++ .../UserSuggestionCoordinatorParameters.swift | 23 +++++ .../Model/UserSuggestionStateAction.swift | 24 +++++ .../Model/UserSuggestionViewAction.swift | 24 +++++ .../Model/UserSuggestionViewModelResult.swift | 24 +++++ .../Model/UserSuggestionViewState.swift | 32 +++++++ .../MatrixSDK/UserSuggestionService.swift | 42 +++++++++ .../Mock/MockUserSuggestionScreenState.swift | 55 +++++++++++ .../Mock/MockUserSuggestionService.swift | 59 ++++++++++++ .../UserSuggestionServiceProtocol.swift | 47 ++++++++++ .../Test/UI/UserSuggestionUITests.swift | 47 ++++++++++ .../Unit/UserSuggestionViewModelTests.swift | 42 +++++++++ .../View/UserSuggestionList.swift | 92 +++++++++++++++++++ .../View/UserSuggestionListItem.swift | 63 +++++++++++++ .../View/UserSuggestionListWithInput.swift | 59 ++++++++++++ .../ViewModel/UserSuggestionViewModel.swift | 87 ++++++++++++++++++ .../UserSuggestionViewModelProtocol.swift | 29 ++++++ .../MockTemplateUserProfileScreenState.swift | 1 - 23 files changed, 864 insertions(+), 4 deletions(-) create mode 100644 RiotSwiftUI/Modules/Common/Util/RoundedCornerShape.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionStateAction.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewAction.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Service/MatrixSDK/UserSuggestionService.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionService.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionViewModelTests.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModelProtocol.swift diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 075a3f57f..bf44086fd 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -4200,6 +4200,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { [self cancelEventSelection]; } + +- (void)roomInputToolbarViewDidRequestUserSuggestions:(MXKRoomInputToolbarView *)toolbarView +{ + +} #pragma mark - MXKRoomMemberDetailsViewControllerDelegate diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index 5c8fab5ec..3e72f3eb9 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -324,7 +324,11 @@ const CGFloat kComposerContainerTrailingPadding = 12; { NSString *newText = [textView.text stringByReplacingCharactersInRange:range withString:text]; [self updateUIWithTextMessage:newText animated:YES]; - + + if ([text isEqualToString:@"@"]) { + [self.delegate roomInputToolbarViewDidRequestUserSuggestions:self]; + } + return YES; } diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index 38022e7b8..1195cd4ff 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -22,7 +22,8 @@ enum MockAppScreens { static let appScreens: [MockScreenState.Type] = [ MockTemplateUserProfileScreenState.self, MockTemplateRoomListScreenState.self, - MockTemplateRoomChatScreenState.self + MockTemplateRoomChatScreenState.self, + MockUserSuggestionScreenState.self ] } diff --git a/RiotSwiftUI/Modules/Common/Util/RoundedCornerShape.swift b/RiotSwiftUI/Modules/Common/Util/RoundedCornerShape.swift new file mode 100644 index 000000000..69a3cf881 --- /dev/null +++ b/RiotSwiftUI/Modules/Common/Util/RoundedCornerShape.swift @@ -0,0 +1,30 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +@available(iOS 14.0, *) +struct RoundedCornerShape: Shape { + + let radius: CGFloat + let corners: UIRectCorner + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) + return Path(path.cgPath) + } +} diff --git a/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift b/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift index 7ba92ef30..915859165 100644 --- a/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift +++ b/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift @@ -40,7 +40,6 @@ class ViewModelContext: ObservableObject { // MARK: Private - private var cancellables = Set() fileprivate let viewActions: PassthroughSubject // MARK: Public diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift new file mode 100644 index 000000000..cca76b3cd --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -0,0 +1,73 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +/* + 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 UIKit +import SwiftUI + +final class UserSuggestionCoordinator: Coordinator { + + // MARK: - Properties + + // MARK: Private + + private let parameters: UserSuggestionCoordinatorParameters + private let userSuggestionHostingController: UIViewController + + private var userSuggestionService: UserSuggestionServiceProtocol + private var userSuggestionViewModel: UserSuggestionViewModelProtocol + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: (() -> Void)? + + // MARK: - Setup + + @available(iOS 14.0, *) + init(parameters: UserSuggestionCoordinatorParameters) { + self.parameters = parameters + + userSuggestionService = UserSuggestionService(session: parameters.room) + userSuggestionViewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: userSuggestionService) + + let view = UserSuggestionList(viewModel: viewModel.context) + .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + + userSuggestionHostingController = VectorHostingController(rootView: view) + } + + // MARK: - Public + func start() { + MXLog.debug("[UserSuggestionCoordinator] did start.") + userSuggestionViewModel.completion = { [weak self] result in + MXLog.debug("[UserSuggestionCoordinator] UserSuggestionViewModel did complete with result: \(result).") + guard let self = self else { return } + switch result { + case .cancel, .done: + self.completion?() + break + } + } + } + + func toPresentable() -> UIViewController { + return self.userSuggestionHostingController + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift new file mode 100644 index 000000000..c0c15053a --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift @@ -0,0 +1,23 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// 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 + +struct UserSuggestionCoordinatorParameters { + let room: MXRoom +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionStateAction.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionStateAction.swift new file mode 100644 index 000000000..153848943 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionStateAction.swift @@ -0,0 +1,24 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// 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 + +@available(iOS 14.0, *) +enum UserSuggestionStateAction { + case updateWithItems([UserSuggestionItemProtocol]) +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewAction.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewAction.swift new file mode 100644 index 000000000..6352ed5ae --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewAction.swift @@ -0,0 +1,24 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// 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 + +@available(iOS 14, *) +enum UserSuggestionViewAction { + case selectedItem(UserSuggestionViewStateItem) +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift new file mode 100644 index 000000000..fc25fdcb6 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift @@ -0,0 +1,24 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// 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 + +@available(iOS 14, *) +enum UserSuggestionViewModelResult { + case selectedItem(UserSuggestionItemProtocol) +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift new file mode 100644 index 000000000..038852299 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift @@ -0,0 +1,32 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine + +@available(iOS 14.0, *) +struct UserSuggestionViewStateItem: BindableState, Identifiable { + let id: String + let avatar: AvatarInputProtocol? + let displayName: String? +} + +@available(iOS 14.0, *) +struct UserSuggestionViewState: BindableState { + var items: [UserSuggestionViewStateItem] +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/MatrixSDK/UserSuggestionService.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/MatrixSDK/UserSuggestionService.swift new file mode 100644 index 000000000..e95537483 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/MatrixSDK/UserSuggestionService.swift @@ -0,0 +1,42 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine + +@available(iOS 14.0, *) +class UserSuggestionService: UserSuggestionServiceProtocol { + + // MARK: - Properties + + // MARK: Private + + private let room: MXRoom + + // MARK: Public + + // MARK: - Setup + + init(room: MXRoom) { + self.room = room + } + + func processPartialUserName(_ userName: String) { + + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift new file mode 100644 index 000000000..8c9d3b80d --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift @@ -0,0 +1,55 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +@available(iOS 14.0, *) +enum MockUserSuggestionScreenState: MockScreenState, CaseIterable { + case multipleResults + case oneResult + case empty + + var screenType: Any.Type { + MockUserSuggestionScreenState.self + } + + var screenView: ([Any], AnyView) { + let service: MockUserSuggestionService + switch self { + case .empty: + service = MockUserSuggestionService(userCount: 0) + case .oneResult: + service = MockUserSuggestionService(userCount: 1) + case .multipleResults: + service = MockUserSuggestionService(userCount: 10) + } + + let listViewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: service) + + let viewModel = UserSuggestionListWithInputViewModel(listViewModel: listViewModel) { partialUserName in + service.processPartialUserName(partialUserName) + } + + return ( + [service, listViewModel], + AnyView(UserSuggestionListWithInput(viewModel: viewModel) + .addDependency(MockAvatarService.example)) + ) + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionService.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionService.swift new file mode 100644 index 000000000..8460646d5 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionService.swift @@ -0,0 +1,59 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine + +@available(iOS 14.0, *) +struct MockUserSuggestionServiceItem: UserSuggestionItemProtocol { + let userId: String + let displayName: String? + let avatarUrl: String? +} + +@available(iOS 14.0, *) +class MockUserSuggestionService: UserSuggestionServiceProtocol { + private var suggestionItems: [UserSuggestionItemProtocol] = [] + + var items: CurrentValueSubject<[UserSuggestionItemProtocol], Never> + + init(userCount: UInt) { + items = CurrentValueSubject([]) + generateUsersWithCount(userCount) + items.send(suggestionItems) + } + + func processPartialUserName(_ userName: String) { + guard userName.count > 0 else { + items.send(suggestionItems) + return + } + + items.send(suggestionItems.filter({ userSuggestion in + return (userSuggestion.displayName?.lowercased().range(of: userName.lowercased()) != .none) + })) + } + + private func generateUsersWithCount(_ count: UInt) { + suggestionItems.removeAll() + for _ in 0.. { get } + + func processPartialUserName(_ userName: String) +} + +// MARK: Avatarable + +@available(iOS 14.0, *) +extension UserSuggestionItemProtocol { + var mxContentUri: String? { + avatarUrl + } + var matrixItemId: String { + userId + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift new file mode 100644 index 000000000..f3f984791 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift @@ -0,0 +1,47 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import RiotSwiftUI + +@available(iOS 14.0, *) +class UserSuggestionUITests: MockScreenTest { + + override class var screenType: MockScreenState.Type { + return MockUserSuggestionScreenState.self + } + + override class func createTest() -> MockScreenTest { + return UserSuggestionUITests(selector: #selector(verifyUserSuggestionScreen)) + } + + func verifyUserSuggestionScreen() throws { + guard let screenState = screenState as? MockUserSuggestionScreenState else { fatalError("no screen") } + switch screenState { + case .longDisplayName(let name): + verifyUserSuggestionLongName(name: name) + } + } + + func verifyUserSuggestionLongName(name: String) { + let displayNameText = app.staticTexts["displayNameText"] + XCTAssert(displayNameText.exists) + XCTAssertEqual(displayNameText.label, name) + } + +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionViewModelTests.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionViewModelTests.swift new file mode 100644 index 000000000..7e9060bf0 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionViewModelTests.swift @@ -0,0 +1,42 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Combine + +@testable import RiotSwiftUI + +@available(iOS 14.0, *) +class UserSuggestionViewModelTests: XCTestCase { + private enum Constants { + static let displayName = "Alice" + } + var service: MockUserSuggestionService! + var viewModel: UserSuggestionViewModelProtocol! + var context: UserSuggestionViewModelType.Context! + var cancellables = Set() + override func setUpWithError() throws { + service = MockUserSuggestionService(userCount: 10) + viewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: service) + context = viewModel.context + } + + func testInitialState() { +// XCTAssertEqual(context.viewState.displayName, Constants.displayName) + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift new file mode 100644 index 000000000..ddf8dcb31 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift @@ -0,0 +1,92 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@available(iOS 14.0, *) +struct UserSuggestionList: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + @ObservedObject var viewModel: UserSuggestionViewModel.Context + let rowHeight: CGFloat = 60.0 + let maxHeight: CGFloat = 300.0 + + var body: some View { + BackgroundView { + ScrollViewReader { scrollViewReader in + List(viewModel.viewState.items) { item in + UserSuggestionListItem( + avatar: item.avatar, + displayName: item.displayName, + userId: item.id + ) + .padding([.top, .bottom], 4.0) + .onTapGesture { + viewModel.send(viewAction: .selectedItem(item)) + } + } + .environment(\.defaultMinListRowHeight, rowHeight) + .frame(height: min(maxHeight, rowHeight * CGFloat(viewModel.viewState.items.count))) + .onAppear(perform: { + guard let lastItemId = viewModel.viewState.items.last?.id else { + return + } + + scrollViewReader.scrollTo(lastItemId) + }) + } + } + } +} + +@available(iOS 14.0, *) +private struct BackgroundView: View { + + var content: () -> Content + + @Environment(\.theme) private var theme: ThemeSwiftUI + + init(@ViewBuilder content: @escaping () -> Content) { + self.content = content + } + + var body: some View { + VStack(content: content) + .background(theme.colors.background) + .clipShape(RoundedCornerShape(radius: 20.0, corners: [.topLeft, .topRight])) + .shadow(color: .black.opacity(0.20), radius: 20.0, x: 0.0, y: 3.0) + .edgesIgnoringSafeArea(.all) + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct UserSuggestion_Previews: PreviewProvider { + static let stateRenderer = MockUserSuggestionScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup() + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift new file mode 100644 index 000000000..927155a73 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift @@ -0,0 +1,63 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@available(iOS 14.0, *) +struct UserSuggestionListItem: View { + + // MARK: - Properties + + // MARK: Private + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + let avatar: AvatarInputProtocol? + let displayName: String? + let userId: String + + var body: some View { + HStack { + if let avatar = avatar { + AvatarImage(avatarData: avatar, size: .medium) + } + VStack(alignment:.leading) { + Text(displayName ?? "") + .font(theme.fonts.body) + .foregroundColor(theme.colors.primaryContent) + .accessibility(identifier: "displayNameText") + .lineLimit(1) + Text(userId) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.tertiaryContent) + .accessibility(identifier: "userIdText") + .lineLimit(1) + } + } + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct UserSuggestionHeader_Previews: PreviewProvider { + static var previews: some View { + UserSuggestionListItem(avatar: MockAvatarInput.example, displayName: "Alice", userId: "@alice:matrix.org") + .addDependency(MockAvatarService.example) + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift new file mode 100644 index 000000000..4d34f8f34 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift @@ -0,0 +1,59 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@available(iOS 14.0, *) +struct UserSuggestionListWithInputViewModel { + let listViewModel: UserSuggestionViewModelProtocol + let callback: (String)->() +} + +@available(iOS 14.0, *) +struct UserSuggestionListWithInput: View { + + // MARK: - Properties + + // MARK: Private + + // MARK: Public + + var viewModel: UserSuggestionListWithInputViewModel + @State private var inputText: String = "" + + var body: some View { + VStack(spacing: 0.0) { + UserSuggestionList(viewModel: viewModel.listViewModel.context) + TextField("Search for user", text: $inputText) + .background(Color.white) + .onChange(of: inputText, perform: { value in + viewModel.callback(value) + }) + .border(Color.black) + .padding([.leading, .trailing]) + } + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct UserSuggestionListWithInput_Previews: PreviewProvider { + static let stateRenderer = MockUserSuggestionScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup() + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift new file mode 100644 index 000000000..48ac36b81 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift @@ -0,0 +1,87 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import Combine + +@available(iOS 14, *) +typealias UserSuggestionViewModelType = StateStoreViewModel +@available(iOS 14, *) +class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewModelProtocol { + + // MARK: - Properties + + // MARK: Private + + private let userSuggestionService: UserSuggestionServiceProtocol + + // MARK: Public + + var completion: ((UserSuggestionViewModelResult) -> Void)? + + // MARK: - Setup + + static func makeUserSuggestionViewModel(userSuggestionService: UserSuggestionServiceProtocol) -> UserSuggestionViewModelProtocol { + return UserSuggestionViewModel(userSuggestionService: userSuggestionService) + } + + deinit { + print("well shit") + } + + private init(userSuggestionService: UserSuggestionServiceProtocol) { + self.userSuggestionService = userSuggestionService + super.init(initialViewState: Self.defaultState(userSuggestionService: userSuggestionService)) + setupItemsObserving() + } + + private func setupItemsObserving() { + let updatePublisher = userSuggestionService.items + .map(UserSuggestionStateAction.updateWithItems) + .eraseToAnyPublisher() + dispatch(actionPublisher: updatePublisher) + } + + private static func defaultState(userSuggestionService: UserSuggestionServiceProtocol) -> UserSuggestionViewState { + let viewStateItems = userSuggestionService.items.value.map { suggestionItem in + return UserSuggestionViewStateItem(id: suggestionItem.userId, avatar: suggestionItem, displayName: suggestionItem.displayName) + } + + return UserSuggestionViewState(items: viewStateItems) + } + + // MARK: - Public + + override func process(viewAction: UserSuggestionViewAction) { + switch viewAction { + case .selectedItem(_): + break + } + } + + override class func reducer(state: inout UserSuggestionViewState, action: UserSuggestionStateAction) { + switch action { + case .updateWithItems(let items): + state.items = items.map({ item in + UserSuggestionViewStateItem(id: item.userId, avatar: item, displayName: item.displayName) + }) + } + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModelProtocol.swift new file mode 100644 index 000000000..10207210c --- /dev/null +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModelProtocol.swift @@ -0,0 +1,29 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// 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 + +@available(iOS 14, *) +protocol UserSuggestionViewModelProtocol { + + static func makeUserSuggestionViewModel(userSuggestionService: UserSuggestionServiceProtocol) -> UserSuggestionViewModelProtocol + + var context: UserSuggestionViewModelType.Context { get } + + var completion: ((UserSuggestionViewModelResult) -> Void)? { get set } +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileScreenState.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileScreenState.swift index ae0dbd208..f9e25cadc 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileScreenState.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileScreenState.swift @@ -17,7 +17,6 @@ import Foundation import SwiftUI - /// Using an enum for the screen allows you define the different state cases with /// the relevant associated data for each case. @available(iOS 14.0, *) From c06e682e23b2e5c08235c3270d859ae778d0a98c Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 4 Oct 2021 11:37:37 +0300 Subject: [PATCH 05/23] #1098 - Added user suggestions to the main app timeline. --- Riot/Modules/Room/RoomViewController.m | 25 ++++++++++ Riot/Modules/Room/RoomViewController.xib | 24 +++++++--- .../UserSuggestionCoordinator.swift | 24 ++++------ .../UserSuggestionCoordinatorBridge.swift | 48 +++++++++++++++++++ .../UserSuggestionCoordinatorParameters.swift | 1 + .../MatrixSDK/UserSuggestionService.swift | 23 +++++++++ .../Mock/MockUserSuggestionScreenState.swift | 6 --- .../View/UserSuggestionList.swift | 5 +- 8 files changed, 129 insertions(+), 27 deletions(-) create mode 100644 RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index bf44086fd..60689bc21 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -249,6 +249,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @property (nonatomic, strong) VoiceMessageController *voiceMessageController; @property (nonatomic, strong) SpaceDetailPresenter *spaceDetailPresenter; +@property (nonatomic, strong) UserSuggestionCoordinatorBridge *userSuggestionCoordinator; +@property (nonatomic, weak) IBOutlet UIView *userSuggestionContainerView; + @end @implementation RoomViewController @@ -452,12 +455,31 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self userInterfaceThemeDidChange]; }]; + [self userInterfaceThemeDidChange]; // Observe URL preview updates. [self registerURLPreviewNotifications]; [self setupActions]; + + [self setupUserSuggestionView]; +} + +- (void)setupUserSuggestionView +{ + UIViewController *suggestionsViewController = self.userSuggestionCoordinator.toPresentable; + [suggestionsViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO]; + + [self addChildViewController:suggestionsViewController]; + [self.userSuggestionContainerView addSubview:suggestionsViewController.view]; + + [NSLayoutConstraint activateConstraints:@[[suggestionsViewController.view.topAnchor constraintEqualToAnchor:self.userSuggestionContainerView.topAnchor], + [suggestionsViewController.view.leadingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.leadingAnchor], + [suggestionsViewController.view.trailingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.trailingAnchor], + [suggestionsViewController.view.bottomAnchor constraintEqualToAnchor:self.userSuggestionContainerView.bottomAnchor],]]; + + [suggestionsViewController didMoveToParentViewController:self]; } - (void)userInterfaceThemeDidChange @@ -1019,6 +1041,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [VoiceMessageMediaServiceProvider.sharedProvider setCurrentRoomSummary:dataSource.room.summary]; _voiceMessageController.roomId = dataSource.roomId; + + _userSuggestionCoordinator = [[UserSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager + room:dataSource.room]; } - (void)onRoomDataSourceReady diff --git a/Riot/Modules/Room/RoomViewController.xib b/Riot/Modules/Room/RoomViewController.xib index 5b1a0a909..1c8f83913 100644 --- a/Riot/Modules/Room/RoomViewController.xib +++ b/Riot/Modules/Room/RoomViewController.xib @@ -1,9 +1,9 @@ - + - + @@ -32,6 +32,7 @@ + @@ -136,14 +137,14 @@