vector-im/element-ios/issues/5114 - Poll creation screen

- added input toolbar poll creation action.
- reordered input toolbar actions as per designs.
- added multiline text field and extracted common components.
This commit is contained in:
Stefan Ceriu
2021-11-01 16:52:00 +02:00
committed by Stefan Ceriu
parent 01188f593e
commit ba9c40cf2d
50 changed files with 1231 additions and 42 deletions

View File

@@ -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

View File

@@ -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)
}
}

17
Podfile
View File

@@ -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

View File

@@ -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

View File

@@ -4,7 +4,8 @@
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
buildImplicitDependencies = "YES"
runPostActionsOnFailure = "NO">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "action_poll.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "action_poll@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "action_poll@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "poll_delete_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "poll_delete_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "poll_delete_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "poll_delete_option_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "poll_delete_option_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "poll_delete_option_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "poll_edit_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "poll_edit_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "poll_edit_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "poll_end_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "poll_end_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "poll_end_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 B

View File

@@ -1780,3 +1780,19 @@ Tap the + to start adding people.";
"version_check_modal_title_deprecated" = "Were no longer supporting iOS %@";
"version_check_modal_subtitle_deprecated" = "We've been working on enhancing %@ for a faster and more polished experience. Unfortunately your current version of iOS is not compatible with some of those fixes and is no longer supported.\nWe're advising you to upgrade your operating system to use %@ to its full potential.";
"version_check_modal_action_title_deprecated" = "Find out how";
// Mark: - Polls
"poll_edit_form_create_poll" = "Create poll";
"poll_edit_form_poll_question_or_topic" = "Poll question or topic";
"poll_edit_form_question_or_topic" = "Question or topic";
"poll_edit_form_input_placeholder" = "Write something";
"poll_edit_form_create_options" = "Create options";
"poll_edit_form_option_number" = "Option %d";
"poll_edit_form_add_option" = "Add option";

View File

@@ -114,6 +114,7 @@ internal enum Asset {
internal static let actionCamera = ImageAsset(name: "action_camera")
internal static let actionFile = ImageAsset(name: "action_file")
internal static let actionMediaLibrary = ImageAsset(name: "action_media_library")
internal static let actionPoll = ImageAsset(name: "action_poll")
internal static let actionSticker = ImageAsset(name: "action_sticker")
internal static let error = ImageAsset(name: "error")
internal static let errorMessageTick = ImageAsset(name: "error_message_tick")
@@ -142,6 +143,10 @@ internal enum Asset {
internal static let videoCall = ImageAsset(name: "video_call")
internal static let voiceCallHangonIcon = ImageAsset(name: "voice_call_hangon_icon")
internal static let voiceCallHangupIcon = ImageAsset(name: "voice_call_hangup_icon")
internal static let pollDeleteIcon = ImageAsset(name: "poll_delete_icon")
internal static let pollDeleteOptionIcon = ImageAsset(name: "poll_delete_option_icon")
internal static let pollEditIcon = ImageAsset(name: "poll_edit_icon")
internal static let pollEndIcon = ImageAsset(name: "poll_end_icon")
internal static let urlPreviewClose = ImageAsset(name: "url_preview_close")
internal static let urlPreviewCloseDark = ImageAsset(name: "url_preview_close_dark")
internal static let voiceMessageCancelGradient = ImageAsset(name: "voice_message_cancel_gradient")

View File

@@ -2363,6 +2363,34 @@ public class VectorL10n: NSObject {
public static func pinProtectionSettingsSectionHeaderWithBiometrics(_ p1: String) -> 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")

View File

@@ -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

View File

@@ -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()
}
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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"

View File

@@ -23,7 +23,8 @@ enum MockAppScreens {
MockTemplateUserProfileScreenState.self,
MockTemplateRoomListScreenState.self,
MockTemplateRoomChatScreenState.self,
MockUserSuggestionScreenState.self
MockUserSuggestionScreenState.self,
MockPollEditFormScreenState.self
]
}

View File

@@ -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)
}
}
}

View File

@@ -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<String>) {
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<UITextViewWrapper>) -> 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<UITextViewWrapper>) {
if uiView.text != self.text {
uiView.text = self.text
}
UITextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight)
}
fileprivate static func recalculateHeight(view: UIView, result: Binding<CGFloat>) {
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<String>
var calculatedHeight: Binding<CGFloat>
var isEditing: Binding<Bool>
init(text: Binding<String>, height: Binding<CGFloat>, isEditing: Binding<Bool>) {
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)
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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<T: RandomAccessCollection & MutableCollection, C: View>: View {
typealias BoundElement = Binding<T.Element>
private let binding: BoundElement
private let content: (BoundElement) -> C
init(_ binding: Binding<T>, 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)
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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]
}

View File

@@ -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)))
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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))
}
}
}

View File

@@ -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<AnyCancellable>()
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)
}
}

View File

@@ -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..<viewModel.answerOptions.count, id: \.self) { index in
SafeBindingCollectionEnumerator($viewModel.answerOptions, index: index) { binding in
AnswerOptionGroup(text: binding.text, index: index) {
viewModel.send(viewAction: .deleteAnswerOption(viewModel.answerOptions[index]))
}
}
}
}
Button(VectorL10n.pollEditFormAddOption) {
viewModel.send(viewAction: .addAnswerOption)
}
.disabled(!viewModel.viewState.addAnswerOptionButtonEnabled)
Spacer()
Button(VectorL10n.pollEditFormCreatePoll) {
viewModel.send(viewAction: .create)
}
.buttonStyle(PrimaryActionButtonStyle(enabled: viewModel.viewState.confirmationButtonEnabled))
.disabled(!viewModel.viewState.confirmationButtonEnabled)
}
.animation(.easeInOut(duration: 0.2))
.padding()
.frame(minHeight: proxy.size.height) // Make the VStack fill the ScrollView's parent
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(VectorL10n.cancel, action: {
viewModel.send(viewAction: .cancel)
})
}
ToolbarItem(placement: .principal) {
Text(VectorL10n.pollEditFormCreatePoll)
.font(.headline)
.foregroundColor(theme.colors.primaryContent)
}
}
.navigationBarTitleDisplayMode(.inline)
}
}
}
.accentColor(theme.colors.accent)
}
}
@available(iOS 14.0, *)
private struct AnswerOptionGroup: View {
@Environment(\.theme) private var theme: ThemeSwiftUI
@State private var focused = false
@Binding var text: String
let index: Int
let onDelete: () -> 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()
}
}

View File

@@ -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

View File

@@ -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: "")
}))
}
}

View File

@@ -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"

View File

@@ -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 {