diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 6c15d412c..2d080c6d2 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -288,6 +288,13 @@ final class BuildSettings: NSObject { static let roomScreenAllowMediaLibraryAction: Bool = true static let roomScreenAllowStickerAction: Bool = true static let roomScreenAllowFilesAction: Bool = true + static var roomScreenAllowPollsAction: Bool { + guard #available(iOS 14, *) else { + return false + } + + return false + } /// Allow split view detail view stacking static let allowSplitViewDetailsScreenStacking: Bool = true diff --git a/DesignKit/Source/FontsSwiftUI.swift b/DesignKit/Source/FontsSwiftUI.swift index 223936286..ddf6a1754 100644 --- a/DesignKit/Source/FontsSwiftUI.swift +++ b/DesignKit/Source/FontsSwiftUI.swift @@ -22,6 +22,9 @@ import SwiftUI */ @available(iOS 14.0, *) public struct FontSwiftUI: Fonts { + + public let uiFonts: ElementFonts + public var largeTitle: Font public var largeTitleB: Font @@ -63,6 +66,8 @@ public struct FontSwiftUI: Fonts { public var caption2SB: Font public init(values: ElementFonts) { + self.uiFonts = values + self.largeTitle = Font(values.largeTitle) self.largeTitleB = Font(values.largeTitleB) self.title1 = Font(values.title1) @@ -85,4 +90,3 @@ public struct FontSwiftUI: Fonts { self.caption2SB = Font(values.caption2SB) } } - diff --git a/Podfile b/Podfile index f2fab251b..4cf944f90 100644 --- a/Podfile +++ b/Podfile @@ -43,6 +43,10 @@ end ######################################## +def import_SwiftUI_pods + pod 'Introspect', '~> 0.1' +end + abstract_target 'RiotPods' do pod 'GBDeviceInfo', '~> 6.6.0' @@ -63,6 +67,9 @@ abstract_target 'RiotPods' do target "Riot" do import_MatrixKit + + import_SwiftUI_pods + pod 'DGCollectionViewLeftAlignFlowLayout', '~> 1.0.4' pod 'KTCenterFlowLayout', '~> 1.3.1' pod 'ZXingObjC', '~> 3.6.5' @@ -73,7 +80,7 @@ abstract_target 'RiotPods' do pod 'SideMenu', '~> 6.5' pod 'DSWaveformImage', '~> 6.1.1' pod 'ffmpeg-kit-ios-audio', '~> 4.5' - + pod 'FLEX', '~> 4.5.0', :configurations => ['Debug'] target 'RiotTests' do @@ -85,6 +92,14 @@ abstract_target 'RiotPods' do import_MatrixKit end + target "RiotSwiftUI" do + import_SwiftUI_pods + end + + target "RiotSwiftUITests" do + import_SwiftUI_pods + end + target "SiriIntents" do import_MatrixKit end diff --git a/Podfile.lock b/Podfile.lock index 00be05819..9e25bd5ec 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -16,7 +16,7 @@ PODS: - AFNetworking/NSURLSession - BlueCryptor (1.0.32) - BlueECC (1.2.5) - - BlueRSA (1.0.34) + - BlueRSA (1.0.200) - DGCollectionViewLeftAlignFlowLayout (1.0.4) - Down (0.11.0) - DSWaveformImage (6.1.1) @@ -39,13 +39,13 @@ PODS: - DTFoundation/Core - ffmpeg-kit-ios-audio (4.5) - FLEX (4.5.0) - - FlowCommoniOS (1.12.0) + - FlowCommoniOS (1.12.2) - GBDeviceInfo (6.6.0): - GBDeviceInfo/Core (= 6.6.0) - GBDeviceInfo/Core (6.6.0) - - GrowingTextView (0.7.2) - GZIP (1.3.0) - HPGrowingTextView (1.1) + - Introspect (0.1.3) - JitsiMeetSDK (3.10.2) - KeychainAccess (4.2.2) - KituraContracts (1.2.1): @@ -100,7 +100,7 @@ PODS: - Reusable/View (4.1.2) - SideMenu (6.5.0) - SwiftBase32 (0.9.0) - - SwiftGen (6.4.0) + - SwiftGen (6.5.1) - SwiftJWT (3.6.200): - BlueCryptor (~> 1.0) - BlueECC (~> 1.1) @@ -122,7 +122,7 @@ DEPENDENCIES: - FLEX (~> 4.5.0) - FlowCommoniOS (~> 1.12.0) - GBDeviceInfo (~> 6.6.0) - - GrowingTextView (~> 0.7.2) + - Introspect (~> 0.1) - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - MatomoTracker (~> 7.4.1) @@ -156,9 +156,9 @@ SPEC REPOS: - FLEX - FlowCommoniOS - GBDeviceInfo - - GrowingTextView - GZIP - HPGrowingTextView + - Introspect - JitsiMeetSDK - KeychainAccess - KituraContracts @@ -188,7 +188,7 @@ SPEC CHECKSUMS: AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce BlueCryptor: b0aee3d9b8f367b49b30de11cda90e1735571c24 BlueECC: 0d18e93347d3ec6d41416de21c1ffa4d4cd3c2cc - BlueRSA: 6f9776d62d9773502415a7db3bcbb2bbb3f71fc3 + BlueRSA: dfeef51db96bcc4edec654956c1581adbda4e6a3 DGCollectionViewLeftAlignFlowLayout: a0fa58797373ded039cafba8133e79373d048399 Down: b6ba1bc985c9d2f4e15e3b293d2207766fa12612 DSWaveformImage: 3c718a0cf99291887ee70d1d0c18d80101d3d9ce @@ -196,11 +196,11 @@ SPEC CHECKSUMS: DTFoundation: a53f8cda2489208cbc71c648be177f902ee17536 ffmpeg-kit-ios-audio: 8c44d93054e1a9743a7014ec3dd26cd1ad8f2a59 FLEX: e51461dd6f0bfb00643c262acdfea5d5d12c596b - FlowCommoniOS: e9ecbc97fb9ce5c593fb3da0e1073b65a3902026 + FlowCommoniOS: ca92071ab526dc89905495a37844fd7e78d1a7f2 GBDeviceInfo: ed0db16230d2fa280e1cbb39a5a7f60f6946aaec - GrowingTextView: 876bf42005b5e4a4fd740597db12caaf41f0fe6c GZIP: 416858efbe66b41b206895ac6dfd5493200d95b3 HPGrowingTextView: 88a716d97fb853bcb08a4a08e4727da17efc9b19 + Introspect: 2be020f30f084ada52bb4387fff83fa52c5c400e JitsiMeetSDK: 2f118fa770f23e518f3560fc224fae3ac7062223 KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51 KituraContracts: e845e60dc8627ad0a76fa55ef20a45451d8f830b @@ -218,7 +218,7 @@ SPEC CHECKSUMS: Reusable: 6bae6a5e8aa793c9c441db0213c863a64bce9136 SideMenu: f583187d21c5b1dd04c72002be544b555a2627a2 SwiftBase32: 9399c25a80666dc66b51e10076bf591e3bbb8f17 - SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 + SwiftGen: a6d22010845f08fe18fbdf3a07a8e380fd22e0ea SwiftJWT: 88c412708f58c169d431d344c87bc79a87c830ae SwiftLint: e96c0a8c770c7ebbc4d36c55baf9096bb65c4584 SwiftyBeaver: 84069991dd5dca07d7069100985badaca7f0ce82 @@ -226,6 +226,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 2740772a9b2d32e17876526875dfc58f67240ba0 +PODFILE CHECKSUM: 56547a5087a419eb4461b0fb59380381194392dc COCOAPODS: 1.11.2 diff --git a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme index 84ecb908a..a9bea1d96 100644 --- a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme +++ b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme @@ -4,7 +4,8 @@ version = "1.3"> + buildImplicitDependencies = "YES" + runPostActionsOnFailure = "NO"> String { return VectorL10n.tr("Vector", "pin_protection_settings_section_header_with_biometrics", p1) } + /// Add option + public static var pollEditFormAddOption: String { + return VectorL10n.tr("Vector", "poll_edit_form_add_option") + } + /// Create options + public static var pollEditFormCreateOptions: String { + return VectorL10n.tr("Vector", "poll_edit_form_create_options") + } + /// Create poll + public static var pollEditFormCreatePoll: String { + return VectorL10n.tr("Vector", "poll_edit_form_create_poll") + } + /// Write something + public static var pollEditFormInputPlaceholder: String { + return VectorL10n.tr("Vector", "poll_edit_form_input_placeholder") + } + /// Option %d + public static func pollEditFormOptionNumber(_ p1: Int) -> String { + return VectorL10n.tr("Vector", "poll_edit_form_option_number", p1) + } + /// Poll question or topic + public static var pollEditFormPollQuestionOrTopic: String { + return VectorL10n.tr("Vector", "poll_edit_form_poll_question_or_topic") + } + /// Question or topic + public static var pollEditFormQuestionOrTopic: String { + return VectorL10n.tr("Vector", "poll_edit_form_question_or_topic") + } /// Preview public static var preview: String { return VectorL10n.tr("Vector", "preview") diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 9e1ccd05b..4b372d460 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -165,7 +165,7 @@ final class RiotSettings: NSObject { @UserDefault(key: "roomScreenAllowFilesAction", defaultValue: BuildSettings.roomScreenAllowFilesAction, storage: defaults) var roomScreenAllowFilesAction - + @UserDefault(key: "roomScreenShowsURLPreviews", defaultValue: true, storage: defaults) var roomScreenShowsURLPreviews diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index 4764e03e9..fa9a5d2db 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -29,6 +29,8 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { private let roomViewController: RoomViewController private let activityIndicatorPresenter: ActivityIndicatorPresenterType private var selectedEventId: String? + + private var pollEditFormCoordinator: PollEditFormCoordinator? private var roomDataSourceManager: MXKRoomDataSourceManager { return MXKRoomDataSourceManager.sharedManager(forMatrixSession: self.parameters.session) @@ -240,4 +242,16 @@ extension RoomCoordinator: RoomViewControllerDelegate { func roomViewController(_ roomViewController: RoomViewController, handleUniversalLinkWith parameters: UniversalLinkParameters) -> Bool { return AppDelegate.theDelegate().handleUniversalLink(with: parameters) } + + func roomViewControllerDidRequestPollCreationFormPresentation(_ roomViewController: RoomViewController) { + guard #available(iOS 14.0, *) else { + return + } + + let parameters = PollEditFormCoordinatorParameters(navigationRouter: self.navigationRouter) + + pollEditFormCoordinator = PollEditFormCoordinator(parameters: parameters) + + pollEditFormCoordinator?.start() + } } diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index 5016dec0f..3723eb5d8 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -176,6 +176,13 @@ extern NSNotificationName const RoomGroupCallTileTappedNotification; - (BOOL)roomViewController:(RoomViewController *)roomViewController handleUniversalLinkWithParameters:(UniversalLinkParameters*)parameters; +/** + Ask the coordinator to invoke the poll creation form coordinator. + + @param roomViewController the `RoomViewController` instance. + */ +- (void)roomViewControllerDidRequestPollCreationFormPresentation:(RoomViewController *)roomViewController; + @end NS_ASSUME_NONNULL_END diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index eba7a4ba4..589b6be8c 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -1987,16 +1987,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; RoomInputToolbarView *roomInputView = ((RoomInputToolbarView *) self.inputToolbarView); MXWeakify(self); NSMutableArray *actionItems = [NSMutableArray new]; - if (RiotSettings.shared.roomScreenAllowCameraAction) - { - [actionItems addObject:[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_camera"] andAction:^{ - MXStrongifyAndReturnIfNil(self); - if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) { - ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO; - } - [self showCameraControllerAnimated:YES]; - }]]; - } if (RiotSettings.shared.roomScreenAllowMediaLibraryAction) { [actionItems addObject:[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_media_library"] andAction:^{ @@ -2027,6 +2017,26 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self roomInputToolbarViewDidTapFileUpload]; }]]; } + if (BuildSettings.roomScreenAllowPollsAction) + { + [actionItems addObject:[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_poll"] andAction:^{ + MXStrongifyAndReturnIfNil(self); + if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) { + ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO; + } + [self.delegate roomViewControllerDidRequestPollCreationFormPresentation:self]; + }]]; + } + if (RiotSettings.shared.roomScreenAllowCameraAction) + { + [actionItems addObject:[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_camera"] andAction:^{ + MXStrongifyAndReturnIfNil(self); + if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) { + ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO; + } + [self showCameraControllerAnimated:YES]; + }]]; + } roomInputView.actionsBar.actionItems = actionItems; } diff --git a/RiotSwiftUI/Debug.xcconfig b/RiotSwiftUI/Debug.xcconfig index 11a7288a4..d890f7e35 100644 --- a/RiotSwiftUI/Debug.xcconfig +++ b/RiotSwiftUI/Debug.xcconfig @@ -18,3 +18,4 @@ // https://help.apple.com/xcode/#/dev745c5c974 #include "Common.xcconfig" +#include "Pods/Target Support Files/Pods-RiotPods-RiotSwiftUI/Pods-RiotPods-RiotSwiftUI.debug.xcconfig" diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index 1195cd4ff..51c3e182c 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -23,7 +23,8 @@ enum MockAppScreens { MockTemplateUserProfileScreenState.self, MockTemplateRoomListScreenState.self, MockTemplateRoomChatScreenState.self, - MockUserSuggestionScreenState.self + MockUserSuggestionScreenState.self, + MockPollEditFormScreenState.self ] } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/BorderedInputFieldStyle.swift b/RiotSwiftUI/Modules/Common/Util/BorderedInputFieldStyle.swift similarity index 81% rename from RiotSwiftUI/Modules/Settings/Notifications/View/BorderedInputFieldStyle.swift rename to RiotSwiftUI/Modules/Common/Util/BorderedInputFieldStyle.swift index 89e4349fd..5fbcac5e6 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/BorderedInputFieldStyle.swift +++ b/RiotSwiftUI/Modules/Common/Util/BorderedInputFieldStyle.swift @@ -16,6 +16,7 @@ import Foundation import SwiftUI +import Introspect @available(iOS 14.0, *) /// A bordered style of text input @@ -49,10 +50,11 @@ struct BorderedInputFieldStyle: TextFieldStyle { } private var textColor: Color { - if !isEnabled { - return theme.colors.quarterlyContent + if (theme.identifier == ThemeIdentifier.dark) { + return (isEnabled ? theme.colors.primaryContent : theme.colors.tertiaryContent) + } else { + return (isEnabled ? theme.colors.primaryContent : theme.colors.quarterlyContent) } - return theme.colors.primaryContent } private var backgroundColor: Color { @@ -62,21 +64,31 @@ struct BorderedInputFieldStyle: TextFieldStyle { return theme.colors.background } + private var placeholderColor: Color { + return theme.colors.tertiaryContent + } + private var borderWidth: CGFloat { - return isEditing || isError ? 2 : 1.5 + return isEditing || isError ? 2.0 : 1.5 } func _body(configuration: TextField<_Label>) -> some View { - let rect = RoundedRectangle(cornerRadius: 8) + let rect = RoundedRectangle(cornerRadius: 8.0) return configuration .font(theme.fonts.callout) .foregroundColor(textColor) .accentColor(accentColor) - .frame(height: 48) - .padding(.horizontal, 8) + .frame(height: 48.0) + .padding(.horizontal, 8.0) .background(backgroundColor) .clipShape(rect) .overlay(rect.stroke(borderColor, lineWidth: borderWidth)) + .introspectTextField { textField in + textField.returnKeyType = .done + textField.clearButtonMode = .whileEditing + textField.attributedPlaceholder = NSAttributedString(string: textField.placeholder ?? "", + attributes: [NSAttributedString.Key.foregroundColor: UIColor(placeholderColor)]) + } } } @@ -118,6 +130,5 @@ struct BorderedInputFieldStyle_Previews: PreviewProvider { .padding() .theme(ThemeIdentifier.dark) } - } } diff --git a/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift b/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift new file mode 100644 index 000000000..dd710f70e --- /dev/null +++ b/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift @@ -0,0 +1,207 @@ +// +// 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 MultilineTextField: View { + + @Environment(\.theme) private var theme: ThemeSwiftUI + + @Binding private var text: String + @State private var dynamicHeight: CGFloat = 100 + @State private var isEditing = false + + private var placeholder: String = "" + + private var showingPlaceholder: Bool { + text.isEmpty + } + + init(_ placeholder: String, text: Binding) { + self.placeholder = placeholder + self._text = text + } + + private var textColor: Color { + if (theme.identifier == ThemeIdentifier.dark) { + return theme.colors.primaryContent + } else { + return theme.colors.primaryContent + } + } + + private var backgroundColor: Color { + return theme.colors.background + } + + private var placeholderColor: Color { + return theme.colors.tertiaryContent + } + + private var borderColor: Color { + if isEditing { + return theme.colors.accent + } + + return theme.colors.quarterlyContent + } + + private var borderWidth: CGFloat { + return isEditing ? 2.0 : 1.5 + } + + var body: some View { + let rect = RoundedRectangle(cornerRadius: 8.0) + return UITextViewWrapper(text: $text, calculatedHeight: $dynamicHeight, isEditing: $isEditing) + .frame(minHeight: dynamicHeight, maxHeight: dynamicHeight) + .padding(4.0) + .background(placeholderView, alignment: .topLeading) + .animation(.none) + .background(backgroundColor) + .clipShape(rect) + .overlay(rect.stroke(borderColor, lineWidth: borderWidth)) + .introspectTextView { textView in + textView.textColor = UIColor(textColor) + textView.font = theme.fonts.uiFonts.callout + } + } + + @ViewBuilder + private var placeholderView: some View { + if showingPlaceholder { + Text(placeholder) + .foregroundColor(placeholderColor) + .font(theme.fonts.callout) + .padding(.leading, 8.0) + .padding(.top, 12.0) + } + } +} + +@available(iOS 14.0, *) +fileprivate struct UITextViewWrapper: UIViewRepresentable { + typealias UIViewType = UITextView + + @Binding var text: String + @Binding var calculatedHeight: CGFloat + @Binding var isEditing: Bool + + func makeUIView(context: UIViewRepresentableContext) -> UITextView { + let textView = UITextView() + textView.delegate = context.coordinator + + textView.isEditable = true + textView.font = UIFont.preferredFont(forTextStyle: .body) + textView.isSelectable = true + textView.isUserInteractionEnabled = true + textView.isScrollEnabled = false + textView.backgroundColor = UIColor.clear + textView.returnKeyType = .done + + textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + return textView + } + + func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext) { + if uiView.text != self.text { + uiView.text = self.text + } + + UITextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight) + } + + fileprivate static func recalculateHeight(view: UIView, result: Binding) { + let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude)) + if result.wrappedValue != newSize.height { + DispatchQueue.main.async { + result.wrappedValue = newSize.height // !! must be called asynchronously + } + } + } + + func makeCoordinator() -> Coordinator { + return Coordinator(text: $text, height: $calculatedHeight, isEditing: $isEditing) + } + + final class Coordinator: NSObject, UITextViewDelegate { + var text: Binding + var calculatedHeight: Binding + var isEditing: Binding + + init(text: Binding, height: Binding, isEditing: Binding) { + self.text = text + self.calculatedHeight = height + self.isEditing = isEditing + } + + func textViewDidChange(_ uiView: UITextView) { + text.wrappedValue = uiView.text + UITextViewWrapper.recalculateHeight(view: uiView, result: calculatedHeight) + } + + func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + if text == "\n" { + textView.resignFirstResponder() + return false + } + + return true + } + + func textViewDidBeginEditing(_ textView: UITextView) { + isEditing.wrappedValue = true + } + + func textViewDidEndEditing(_ textView: UITextView) { + isEditing.wrappedValue = false + } + } +} + +@available(iOS 14.0, *) +struct MultilineTextField_Previews: PreviewProvider { + + static var previews: some View { + return Group { + VStack { + PreviewWrapper() + PlaceholderPreviewWrapper() + PreviewWrapper() + .theme(ThemeIdentifier.dark) + PlaceholderPreviewWrapper() + .theme(ThemeIdentifier.dark) + } + } + .padding() + } + + struct PreviewWrapper: View { + @State(initialValue: "123") var text: String + + var body: some View { + MultilineTextField("Placeholder", text: $text) + } + } + + struct PlaceholderPreviewWrapper: View { + @State(initialValue: "") var text: String + + var body: some View { + MultilineTextField("Placeholder", text: $text) + } + } +} diff --git a/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift b/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift new file mode 100644 index 000000000..9d94bac8d --- /dev/null +++ b/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift @@ -0,0 +1,52 @@ +// +// 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 PrimaryActionButtonStyle: ButtonStyle { + @Environment(\.theme) private var theme: ThemeSwiftUI + + var enabled: Bool = false + + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + .padding(12.0) + .frame(maxWidth: .infinity) + .foregroundColor(.white) + .font(theme.fonts.body) + .background(configuration.isPressed ? theme.colors.accent.opacity(0.6) : theme.colors.accent) + .opacity(enabled ? 1.0 : 0.6) + .cornerRadius(8.0) + } +} + +@available(iOS 14.0, *) +struct PrimaryActionButtonStyle_Previews: PreviewProvider { + static var previews: some View { + Group { + VStack { + Button("Enabled") { } + .buttonStyle(PrimaryActionButtonStyle(enabled: true)) + + Button("Disabled") { } + .buttonStyle(PrimaryActionButtonStyle(enabled: false)) + .disabled(true) + } + .padding() + } + } +} diff --git a/RiotSwiftUI/Modules/Common/Util/SafeBindingCollectionEnumerator.swift b/RiotSwiftUI/Modules/Common/Util/SafeBindingCollectionEnumerator.swift new file mode 100644 index 000000000..45ea51855 --- /dev/null +++ b/RiotSwiftUI/Modules/Common/Util/SafeBindingCollectionEnumerator.swift @@ -0,0 +1,40 @@ +// +// 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 + +/** + Used to avoid crashes when enumerating through bindings in the AnswerOptions ForEach + https://stackoverflow.com/q/65375372 + Replace with Swift 5.5 bindings enumerator later. + */ +@available(iOS 14.0, *) +struct SafeBindingCollectionEnumerator: View { + + typealias BoundElement = Binding + private let binding: BoundElement + private let content: (BoundElement) -> C + + init(_ binding: Binding, index: T.Index, @ViewBuilder content: @escaping (BoundElement) -> C) { + self.content = content + self.binding = .init(get: { binding.wrappedValue[index] }, + set: { binding.wrappedValue[index] = $0 }) + } + + var body: some View { + content(binding) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift new file mode 100644 index 000000000..1df71686e --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift @@ -0,0 +1,81 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/PollEditForm PollEditForm +/* + 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 + +struct PollEditFormCoordinatorParameters { + let navigationRouter: NavigationRouterType? +} + +final class PollEditFormCoordinator: Coordinator { + + // MARK: - Properties + + // MARK: Private + + private let parameters: PollEditFormCoordinatorParameters + private let pollEditFormHostingController: UIViewController + private var _pollEditFormViewModel: Any? = nil + + @available(iOS 14.0, *) + fileprivate var pollEditFormViewModel: PollEditFormViewModel { + return _pollEditFormViewModel as! PollEditFormViewModel + } + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + + // MARK: - Setup + + @available(iOS 14.0, *) + init(parameters: PollEditFormCoordinatorParameters) { + self.parameters = parameters + + let viewModel = PollEditFormViewModel() + let view = PollEditForm(viewModel: viewModel.context) + + _pollEditFormViewModel = viewModel + pollEditFormHostingController = VectorHostingController(rootView: view) + } + + // MARK: - Public + func start() { + guard #available(iOS 14.0, *) else { + MXLog.debug("[PollEditFormCoordinator] start: Invalid iOS version, returning.") + return + } + + MXLog.debug("[PollEditFormCoordinator] did start.") + + parameters.navigationRouter?.present(pollEditFormHostingController, animated: true) + + pollEditFormViewModel.completion = { [weak self] result in + guard let self = self else { return } + switch result { + case .cancel: + self.parameters.navigationRouter?.dismissModule(animated: true, completion: nil) + case .create(_, _): + break + } + } + } +} diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift new file mode 100644 index 000000000..9e6657506 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift @@ -0,0 +1,77 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/PollEditForm PollEditForm +// +// 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 + +enum PollEditFormStateAction { + case viewAction(PollEditFormViewAction) +} + +enum PollEditFormViewAction { + case addAnswerOption + case deleteAnswerOption(PollEditFormAnswerOption) + case cancel + case create +} + +enum PollEditFormViewModelResult { + case cancel + case create(String, [String]) +} + +struct PollEditFormQuestion { + var text: String { + didSet { + text = String(text.prefix(maxLength)) + } + } + + let maxLength: Int +} + +struct PollEditFormAnswerOption: Identifiable, Equatable { + let id = UUID() + + var text: String { + didSet { + text = String(text.prefix(maxLength)) + } + } + + let maxLength: Int +} + +struct PollEditFormViewState: BindableState { + let maxAnswerOptionsCount: Int + var bindings: PollEditFormViewStateBindings + + var confirmationButtonEnabled: Bool { + !bindings.question.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && + bindings.answerOptions.filter({ !$0.text.isEmpty }).count >= 2 + } + + var addAnswerOptionButtonEnabled: Bool { + bindings.answerOptions.count < maxAnswerOptionsCount + } +} + +struct PollEditFormViewStateBindings { + var question: PollEditFormQuestion + var answerOptions: [PollEditFormAnswerOption] +} diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift new file mode 100644 index 000000000..2e545a9d4 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift @@ -0,0 +1,34 @@ +// 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 MockPollEditFormScreenState: MockScreenState, CaseIterable { + case standard + + var screenType: Any.Type { + MockPollEditFormScreenState.self + } + + var screenView: ([Any], AnyView) { + let viewModel = PollEditFormViewModel() + return ([viewModel], AnyView(PollEditForm(viewModel: viewModel.context))) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift new file mode 100644 index 000000000..b77274bcb --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift @@ -0,0 +1,91 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/PollEditForm PollEditForm +// +// 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 PollEditFormViewModelType = StateStoreViewModel< PollEditFormViewState, + PollEditFormStateAction, + PollEditFormViewAction > +@available(iOS 14, *) +class PollEditFormViewModel: PollEditFormViewModelType { + + private struct Constants { + static let maxAnswerOptionsCount = 20 + static let maxQuestionLength = 200 + static let maxAnswerOptionLength = 200 + } + + // MARK: - Properties + + // MARK: Private + + // MARK: Public + + var completion: ((PollEditFormViewModelResult) -> Void)? + + // MARK: - Setup + + init() { + super.init(initialViewState: Self.defaultState()) + } + + private static func defaultState() -> PollEditFormViewState { + return PollEditFormViewState( + maxAnswerOptionsCount: Constants.maxAnswerOptionsCount, + bindings: PollEditFormViewStateBindings( + question: PollEditFormQuestion(text: "", maxLength: Constants.maxQuestionLength), + answerOptions: [PollEditFormAnswerOption(text: "", maxLength: Constants.maxAnswerOptionLength), + PollEditFormAnswerOption(text: "", maxLength: Constants.maxAnswerOptionLength) + ] + ) + ) + } + + // MARK: - Public + + override func process(viewAction: PollEditFormViewAction) { + switch viewAction { + case .cancel: + completion?(.cancel) + case .create: + completion?(.create(state.bindings.question.text.trimmingCharacters(in: .whitespacesAndNewlines), + state.bindings.answerOptions.compactMap({ answerOption in + let text = answerOption.text.trimmingCharacters(in: .whitespacesAndNewlines) + return text.isEmpty ? nil : text + }))) + default: + dispatch(action: .viewAction(viewAction)) + } + } + + override class func reducer(state: inout PollEditFormViewState, action: PollEditFormStateAction) { + switch action { + case .viewAction(let viewAction): + switch viewAction { + case .deleteAnswerOption(let answerOption): + state.bindings.answerOptions.removeAll { $0 == answerOption } + case .addAnswerOption: + state.bindings.answerOptions.append(PollEditFormAnswerOption(text: "", maxLength: Constants.maxAnswerOptionLength)) + default: + break + } + } + } +} diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift new file mode 100644 index 000000000..daee82cbd --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift @@ -0,0 +1,82 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/PollEditForm PollEditForm +// +// 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 PollEditFormUITests: XCTestCase { + + private var app: XCUIApplication! + + override func setUp() { + continueAfterFailure = false + + app = XCUIApplication() + app.launch() + app.buttons[MockPollEditFormScreenState.screenStateKeys.first!].tap() + } + + func testInitialStateComponents() { + + XCTAssert(app.scrollViews.firstMatch.exists) + + XCTAssert(app.staticTexts["Create poll"].exists) + XCTAssert(app.staticTexts["Poll question or topic"].exists) + XCTAssert(app.staticTexts["Question or topic"].exists) + XCTAssert(app.staticTexts["Create options"].exists) + + XCTAssert(app.textViews.count == 1) + + XCTAssert(app.textFields.count == 2) + XCTAssert(app.staticTexts["Option 1"].exists) + XCTAssert(app.staticTexts["Option 2"].exists) + + let cancelButton = app.buttons["Cancel"] + XCTAssert(cancelButton.exists) + XCTAssertTrue(cancelButton.isEnabled) + + let addOptionButton = app.buttons["Add option"] + XCTAssert(addOptionButton.exists) + XCTAssertTrue(addOptionButton.isEnabled) + + let createPollButton = app.buttons["Create poll"] + XCTAssert(createPollButton.exists) + XCTAssertFalse(createPollButton.isEnabled) + } + + func testRemoveAddAnswerOptions() { + + let deleteAnswerOptionButton = app.buttons["Delete answer option"].firstMatch + + XCTAssert(deleteAnswerOptionButton.waitForExistence(timeout: 2.0)) + deleteAnswerOptionButton.tap() + + XCTAssert(deleteAnswerOptionButton.waitForExistence(timeout: 2.0)) + deleteAnswerOptionButton.tap() + + let addOptionButton = app.buttons["Add option"] + XCTAssert(addOptionButton.waitForExistence(timeout: 2.0)) + XCTAssertTrue(addOptionButton.isEnabled) + + for i in 1...3 { + addOptionButton.tap() + XCTAssert(app.staticTexts["Option \(i)"].waitForExistence(timeout: 2.0)) + } + } +} diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Test/Unit/PollEditFormViewModelTests.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Test/Unit/PollEditFormViewModelTests.swift new file mode 100644 index 000000000..739361197 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Test/Unit/PollEditFormViewModelTests.swift @@ -0,0 +1,123 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/PollEditForm PollEditForm +// +// 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 PollEditFormViewModelTests: XCTestCase { + var viewModel: PollEditFormViewModel! + var context: PollEditFormViewModelType.Context! + var cancellables = Set() + + override func setUpWithError() throws { + viewModel = PollEditFormViewModel() + context = viewModel.context + } + + func testInitialState() { + XCTAssertTrue(context.question.text.isEmpty) + XCTAssertFalse(context.viewState.confirmationButtonEnabled) + XCTAssertTrue(context.viewState.addAnswerOptionButtonEnabled) + + XCTAssertEqual(context.answerOptions.count, 2) + for answerOption in context.answerOptions { + XCTAssertTrue(answerOption.text.isEmpty) + } + } + + func testDeleteAllAnswerOptions() { + while !context.answerOptions.isEmpty { + context.send(viewAction: .deleteAnswerOption(context.answerOptions.first!)) + } + + XCTAssertEqual(context.answerOptions.count, 0) + XCTAssertFalse(context.viewState.confirmationButtonEnabled) + XCTAssertTrue(context.viewState.addAnswerOptionButtonEnabled) + } + + func testAddRemoveAnswerOption() { + context.send(viewAction: .addAnswerOption) + + XCTAssertEqual(context.answerOptions.count, 3) + + context.send(viewAction: .deleteAnswerOption(context.answerOptions.first!)) + + XCTAssertEqual(context.answerOptions.count, 2) + } + + func testCreateEnabled() { + context.question.text = "Some question" + context.answerOptions[0].text = "First answer" + context.answerOptions[1].text = "Second answer" + + XCTAssertTrue(context.viewState.confirmationButtonEnabled) + } + + func testReachedMaxAnswerOptions() { + for _ in 0...context.viewState.maxAnswerOptionsCount { + context.send(viewAction: .addAnswerOption) + } + + XCTAssertFalse(context.viewState.addAnswerOptionButtonEnabled) + } + + func testQuestionMaxLength() { + let question = String(repeating: "S", count: context.question.maxLength + 100) + context.question.text = question + + XCTAssertEqual(context.question.text.count, context.question.maxLength) + } + + func testAnswerOptionMaxLength() { + let answerOption = String(repeating: "S", count: context.answerOptions[0].maxLength + 100) + context.answerOptions[0].text = answerOption + + XCTAssertEqual(context.answerOptions[0].text.count, context.answerOptions[0].maxLength) + } + + func testFormCompletion() { + let question = "Some question " + let firstAnswer = "First answer " + let secondAnswer = "Second answer " + let thirdAnswer = " " + + viewModel.completion = { result in + if case PollEditFormViewModelResult.create(let resultQuestion, let resultAnswerOptions) = result { + XCTAssertEqual(question.trimmingCharacters(in: .whitespacesAndNewlines), resultQuestion) + + // The last answer option should be automatically dropped as it's empty + XCTAssertEqual(resultAnswerOptions.count, 2) + + XCTAssertEqual(resultAnswerOptions[0], firstAnswer.trimmingCharacters(in: .whitespacesAndNewlines)) + XCTAssertEqual(resultAnswerOptions[1], secondAnswer.trimmingCharacters(in: .whitespacesAndNewlines)) + } + } + + context.question.text = question + context.answerOptions[0].text = firstAnswer + context.answerOptions[1].text = secondAnswer + + context.send(viewAction: .addAnswerOption) + context.answerOptions[2].text = thirdAnswer + + context.send(viewAction: .create) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift new file mode 100644 index 000000000..1d874688b --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift @@ -0,0 +1,146 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/PollEditForm PollEditForm +// +// 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 PollEditForm: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + @ObservedObject var viewModel: PollEditFormViewModel.Context + + var body: some View { + NavigationView { + GeometryReader { proxy in + ScrollView { + VStack(alignment: .leading, spacing: 32.0) { + + VStack(alignment: .leading, spacing: 16.0) { + Text(VectorL10n.pollEditFormPollQuestionOrTopic) + .font(theme.fonts.title3SB) + .foregroundColor(theme.colors.primaryContent) + + VStack(alignment: .leading, spacing: 8.0) { + Text(VectorL10n.pollEditFormQuestionOrTopic) + .font(theme.fonts.subheadline) + .foregroundColor(theme.colors.primaryContent) + + MultilineTextField(VectorL10n.pollEditFormInputPlaceholder, text: $viewModel.question.text) + } + } + + VStack(alignment: .leading, spacing: 16.0) { + Text(VectorL10n.pollEditFormCreateOptions) + .font(theme.fonts.title3SB) + .foregroundColor(theme.colors.primaryContent) + + ForEach(0.. Void + + var body: some View { + VStack(alignment: .leading, spacing: 8.0) { + Text(VectorL10n.pollEditFormOptionNumber(index + 1)) + .font(theme.fonts.subheadline) + .foregroundColor(theme.colors.primaryContent) + + HStack(spacing: 16.0) { + TextField(VectorL10n.pollEditFormInputPlaceholder, text: $text, onEditingChanged: { edit in + self.focused = edit + }) + .textFieldStyle(BorderedInputFieldStyle(theme: _theme, isEditing: focused)) + Button { + onDelete() + } label: { + Image(uiImage:Asset.Images.pollDeleteOptionIcon.image) + } + .accessibilityIdentifier("Delete answer option") + } + } + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct PollEditForm_Previews: PreviewProvider { + static let stateRenderer = MockPollEditFormScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup() + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift index 6e4fab347..1491aa579 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift @@ -61,14 +61,20 @@ class UserSuggestionService: UserSuggestionServiceProtocol { // MARK: - Setup - init(roomMemberProvider: RoomMembersProviderProtocol) { + init(roomMemberProvider: RoomMembersProviderProtocol, shouldDebounce: Bool = true) { self.roomMemberProvider = roomMemberProvider - currentTextTriggerSubject - .debounce(for: 0.5, scheduler: RunLoop.main) - .removeDuplicates() - .sink { [weak self] in self?.fetchAndFilterMembersForTextTrigger($0) } - .store(in: &cancellables) + if (shouldDebounce) { + currentTextTriggerSubject + .debounce(for: 0.5, scheduler: RunLoop.main) + .removeDuplicates() + .sink { [weak self] in self?.fetchAndFilterMembersForTextTrigger($0) } + .store(in: &cancellables) + } else { + currentTextTriggerSubject + .sink { [weak self] in self?.fetchAndFilterMembersForTextTrigger($0) } + .store(in: &cancellables) + } } // MARK: - UserSuggestionServiceProtocol diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift index 1c34f7eb3..fce852059 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift @@ -27,7 +27,7 @@ class UserSuggestionServiceTests: XCTestCase { var service: UserSuggestionService? override func setUp() { - service = UserSuggestionService(roomMembersProvider: self) + service = UserSuggestionService(roomMemberProvider: self, shouldDebounce: false) } func testAlice() { @@ -116,7 +116,7 @@ extension UserSuggestionServiceTests: RoomMembersProviderProtocol { ("Bob", "@bob:matrix.org")] members(users.map({ user in - RoomMembersProviderMember(identifier: user.1, displayName: user.0, avatarURL: "") + RoomMembersProviderMember(userId: user.1, displayName: user.0, avatarUrl: "") })) } } diff --git a/RiotSwiftUI/Release.xcconfig b/RiotSwiftUI/Release.xcconfig index 11a7288a4..561518704 100644 --- a/RiotSwiftUI/Release.xcconfig +++ b/RiotSwiftUI/Release.xcconfig @@ -18,3 +18,4 @@ // https://help.apple.com/xcode/#/dev745c5c974 #include "Common.xcconfig" +#include "Pods/Target Support Files/Pods-RiotPods-RiotSwiftUI/Pods-RiotPods-RiotSwiftUI.release.xcconfig" diff --git a/RiotSwiftUI/RiotSwiftUIApp.swift b/RiotSwiftUI/RiotSwiftUIApp.swift index 63dbece0d..902f7327d 100644 --- a/RiotSwiftUI/RiotSwiftUIApp.swift +++ b/RiotSwiftUI/RiotSwiftUIApp.swift @@ -21,6 +21,14 @@ import SwiftUI struct RiotSwiftUIApp: App { init() { UILog.configure(logger: PrintLogger.self) + + switch UITraitCollection.current.userInterfaceStyle { + case .dark: + ThemePublisher.configure(themeId: .dark) + default: + ThemePublisher.configure(themeId: .light) + } + } var body: some Scene { WindowGroup {