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.
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
22
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
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
buildImplicitDependencies = "YES"
|
||||
runPostActionsOnFailure = "NO">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
|
||||
23
Riot/Assets/Images.xcassets/Room/Actions/action_poll.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
Riot/Assets/Images.xcassets/Room/Actions/action_poll.imageset/action_poll.png
vendored
Normal file
|
After Width: | Height: | Size: 213 B |
BIN
Riot/Assets/Images.xcassets/Room/Actions/action_poll.imageset/action_poll@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 245 B |
BIN
Riot/Assets/Images.xcassets/Room/Actions/action_poll.imageset/action_poll@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 322 B |
6
Riot/Assets/Images.xcassets/Room/Polls/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
23
Riot/Assets/Images.xcassets/Room/Polls/poll_delete_icon.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
Riot/Assets/Images.xcassets/Room/Polls/poll_delete_icon.imageset/poll_delete_icon.png
vendored
Normal file
|
After Width: | Height: | Size: 341 B |
BIN
Riot/Assets/Images.xcassets/Room/Polls/poll_delete_icon.imageset/poll_delete_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 581 B |
BIN
Riot/Assets/Images.xcassets/Room/Polls/poll_delete_icon.imageset/poll_delete_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 787 B |
23
Riot/Assets/Images.xcassets/Room/Polls/poll_delete_option_icon.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
Riot/Assets/Images.xcassets/Room/Polls/poll_delete_option_icon.imageset/poll_delete_option_icon.png
vendored
Normal file
|
After Width: | Height: | Size: 630 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
23
Riot/Assets/Images.xcassets/Room/Polls/poll_edit_icon.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
Riot/Assets/Images.xcassets/Room/Polls/poll_edit_icon.imageset/poll_edit_icon.png
vendored
Normal file
|
After Width: | Height: | Size: 461 B |
BIN
Riot/Assets/Images.xcassets/Room/Polls/poll_edit_icon.imageset/poll_edit_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 763 B |
BIN
Riot/Assets/Images.xcassets/Room/Polls/poll_edit_icon.imageset/poll_edit_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
23
Riot/Assets/Images.xcassets/Room/Polls/poll_end_icon.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
Riot/Assets/Images.xcassets/Room/Polls/poll_end_icon.imageset/poll_end_icon.png
vendored
Normal file
|
After Width: | Height: | Size: 414 B |
BIN
Riot/Assets/Images.xcassets/Room/Polls/poll_end_icon.imageset/poll_end_icon@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 624 B |
BIN
Riot/Assets/Images.xcassets/Room/Polls/poll_end_icon.imageset/poll_end_icon@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 959 B |
@@ -1780,3 +1780,19 @@ Tap the + to start adding people.";
|
||||
"version_check_modal_title_deprecated" = "We’re 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";
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -23,7 +23,8 @@ enum MockAppScreens {
|
||||
MockTemplateUserProfileScreenState.self,
|
||||
MockTemplateRoomListScreenState.self,
|
||||
MockTemplateRoomChatScreenState.self,
|
||||
MockUserSuggestionScreenState.self
|
||||
MockUserSuggestionScreenState.self,
|
||||
MockPollEditFormScreenState.self
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
207
RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
146
RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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: "")
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||