From ba9c40cf2d6951ab2d11aa9fb3ba343014ee2f47 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 1 Nov 2021 16:52:00 +0200 Subject: [PATCH] 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. --- Config/BuildSettings.swift | 7 + DesignKit/Source/FontsSwiftUI.swift | 6 +- Podfile | 17 +- Podfile.lock | 22 +- .../xcshareddata/xcschemes/Riot.xcscheme | 3 +- .../action_poll.imageset/Contents.json | 23 ++ .../action_poll.imageset/action_poll.png | Bin 0 -> 213 bytes .../action_poll.imageset/action_poll@2x.png | Bin 0 -> 245 bytes .../action_poll.imageset/action_poll@3x.png | Bin 0 -> 322 bytes .../Images.xcassets/Room/Polls/Contents.json | 6 + .../poll_delete_icon.imageset/Contents.json | 23 ++ .../poll_delete_icon.png | Bin 0 -> 341 bytes .../poll_delete_icon@2x.png | Bin 0 -> 581 bytes .../poll_delete_icon@3x.png | Bin 0 -> 787 bytes .../Contents.json | 23 ++ .../poll_delete_option_icon.png | Bin 0 -> 630 bytes .../poll_delete_option_icon@2x.png | Bin 0 -> 1142 bytes .../poll_delete_option_icon@3x.png | Bin 0 -> 1693 bytes .../poll_edit_icon.imageset/Contents.json | 23 ++ .../poll_edit_icon.png | Bin 0 -> 461 bytes .../poll_edit_icon@2x.png | Bin 0 -> 763 bytes .../poll_edit_icon@3x.png | Bin 0 -> 1039 bytes .../poll_end_icon.imageset/Contents.json | 23 ++ .../poll_end_icon.imageset/poll_end_icon.png | Bin 0 -> 414 bytes .../poll_end_icon@2x.png | Bin 0 -> 624 bytes .../poll_end_icon@3x.png | Bin 0 -> 959 bytes Riot/Assets/en.lproj/Vector.strings | 16 ++ Riot/Generated/Images.swift | 5 + Riot/Generated/Strings.swift | 28 +++ Riot/Managers/Settings/RiotSettings.swift | 2 +- Riot/Modules/Room/RoomCoordinator.swift | 14 ++ Riot/Modules/Room/RoomViewController.h | 7 + Riot/Modules/Room/RoomViewController.m | 30 ++- RiotSwiftUI/Debug.xcconfig | 1 + .../Modules/Common/Mock/MockAppScreens.swift | 3 +- .../Util}/BorderedInputFieldStyle.swift | 27 ++- .../Common/Util/MultilineTextField.swift | 207 ++++++++++++++++++ .../Util/PrimaryActionButtonStyle.swift | 52 +++++ .../SafeBindingCollectionEnumerator.swift | 40 ++++ .../Coordinator/PollEditFormCoordinator.swift | 81 +++++++ .../PollEditForm/PollEditFormModels.swift | 77 +++++++ .../PollEditFormScreenState.swift | 34 +++ .../PollEditForm/PollEditFormViewModel.swift | 91 ++++++++ .../Test/UI/PollEditFormUITests.swift | 82 +++++++ .../Unit/PollEditFormViewModelTests.swift | 123 +++++++++++ .../Room/PollEditForm/View/PollEditForm.swift | 146 ++++++++++++ .../Service/UserSuggestionService.swift | 18 +- .../Unit/UserSuggestionServiceTests.swift | 4 +- RiotSwiftUI/Release.xcconfig | 1 + RiotSwiftUI/RiotSwiftUIApp.swift | 8 + 50 files changed, 1231 insertions(+), 42 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_poll.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_poll.imageset/action_poll.png create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_poll.imageset/action_poll@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_poll.imageset/action_poll@3x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Polls/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Polls/poll_delete_icon.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Polls/poll_delete_icon.imageset/poll_delete_icon.png create mode 100644 Riot/Assets/Images.xcassets/Room/Polls/poll_delete_icon.imageset/poll_delete_icon@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Polls/poll_delete_icon.imageset/poll_delete_icon@3x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Polls/poll_delete_option_icon.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Polls/poll_delete_option_icon.imageset/poll_delete_option_icon.png create mode 100644 Riot/Assets/Images.xcassets/Room/Polls/poll_delete_option_icon.imageset/poll_delete_option_icon@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Polls/poll_delete_option_icon.imageset/poll_delete_option_icon@3x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Polls/poll_edit_icon.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Polls/poll_edit_icon.imageset/poll_edit_icon.png create mode 100644 Riot/Assets/Images.xcassets/Room/Polls/poll_edit_icon.imageset/poll_edit_icon@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Polls/poll_edit_icon.imageset/poll_edit_icon@3x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Polls/poll_end_icon.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Polls/poll_end_icon.imageset/poll_end_icon.png create mode 100644 Riot/Assets/Images.xcassets/Room/Polls/poll_end_icon.imageset/poll_end_icon@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Polls/poll_end_icon.imageset/poll_end_icon@3x.png rename RiotSwiftUI/Modules/{Settings/Notifications/View => Common/Util}/BorderedInputFieldStyle.swift (81%) create mode 100644 RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift create mode 100644 RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift create mode 100644 RiotSwiftUI/Modules/Common/Util/SafeBindingCollectionEnumerator.swift create mode 100644 RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift create mode 100644 RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift create mode 100644 RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift create mode 100644 RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift create mode 100644 RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift create mode 100644 RiotSwiftUI/Modules/Room/PollEditForm/Test/Unit/PollEditFormViewModelTests.swift create mode 100644 RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 6c15d412c..2d080c6d2 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -288,6 +288,13 @@ final class BuildSettings: NSObject { static let roomScreenAllowMediaLibraryAction: Bool = true static let roomScreenAllowStickerAction: Bool = true static let roomScreenAllowFilesAction: Bool = true + static var roomScreenAllowPollsAction: Bool { + guard #available(iOS 14, *) else { + return false + } + + return false + } /// Allow split view detail view stacking static let allowSplitViewDetailsScreenStacking: Bool = true diff --git a/DesignKit/Source/FontsSwiftUI.swift b/DesignKit/Source/FontsSwiftUI.swift index 223936286..ddf6a1754 100644 --- a/DesignKit/Source/FontsSwiftUI.swift +++ b/DesignKit/Source/FontsSwiftUI.swift @@ -22,6 +22,9 @@ import SwiftUI */ @available(iOS 14.0, *) public struct FontSwiftUI: Fonts { + + public let uiFonts: ElementFonts + public var largeTitle: Font public var largeTitleB: Font @@ -63,6 +66,8 @@ public struct FontSwiftUI: Fonts { public var caption2SB: Font public init(values: ElementFonts) { + self.uiFonts = values + self.largeTitle = Font(values.largeTitle) self.largeTitleB = Font(values.largeTitleB) self.title1 = Font(values.title1) @@ -85,4 +90,3 @@ public struct FontSwiftUI: Fonts { self.caption2SB = Font(values.caption2SB) } } - diff --git a/Podfile b/Podfile index f2fab251b..4cf944f90 100644 --- a/Podfile +++ b/Podfile @@ -43,6 +43,10 @@ end ######################################## +def import_SwiftUI_pods + pod 'Introspect', '~> 0.1' +end + abstract_target 'RiotPods' do pod 'GBDeviceInfo', '~> 6.6.0' @@ -63,6 +67,9 @@ abstract_target 'RiotPods' do target "Riot" do import_MatrixKit + + import_SwiftUI_pods + pod 'DGCollectionViewLeftAlignFlowLayout', '~> 1.0.4' pod 'KTCenterFlowLayout', '~> 1.3.1' pod 'ZXingObjC', '~> 3.6.5' @@ -73,7 +80,7 @@ abstract_target 'RiotPods' do pod 'SideMenu', '~> 6.5' pod 'DSWaveformImage', '~> 6.1.1' pod 'ffmpeg-kit-ios-audio', '~> 4.5' - + pod 'FLEX', '~> 4.5.0', :configurations => ['Debug'] target 'RiotTests' do @@ -85,6 +92,14 @@ abstract_target 'RiotPods' do import_MatrixKit end + target "RiotSwiftUI" do + import_SwiftUI_pods + end + + target "RiotSwiftUITests" do + import_SwiftUI_pods + end + target "SiriIntents" do import_MatrixKit end diff --git a/Podfile.lock b/Podfile.lock index 00be05819..9e25bd5ec 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -16,7 +16,7 @@ PODS: - AFNetworking/NSURLSession - BlueCryptor (1.0.32) - BlueECC (1.2.5) - - BlueRSA (1.0.34) + - BlueRSA (1.0.200) - DGCollectionViewLeftAlignFlowLayout (1.0.4) - Down (0.11.0) - DSWaveformImage (6.1.1) @@ -39,13 +39,13 @@ PODS: - DTFoundation/Core - ffmpeg-kit-ios-audio (4.5) - FLEX (4.5.0) - - FlowCommoniOS (1.12.0) + - FlowCommoniOS (1.12.2) - GBDeviceInfo (6.6.0): - GBDeviceInfo/Core (= 6.6.0) - GBDeviceInfo/Core (6.6.0) - - GrowingTextView (0.7.2) - GZIP (1.3.0) - HPGrowingTextView (1.1) + - Introspect (0.1.3) - JitsiMeetSDK (3.10.2) - KeychainAccess (4.2.2) - KituraContracts (1.2.1): @@ -100,7 +100,7 @@ PODS: - Reusable/View (4.1.2) - SideMenu (6.5.0) - SwiftBase32 (0.9.0) - - SwiftGen (6.4.0) + - SwiftGen (6.5.1) - SwiftJWT (3.6.200): - BlueCryptor (~> 1.0) - BlueECC (~> 1.1) @@ -122,7 +122,7 @@ DEPENDENCIES: - FLEX (~> 4.5.0) - FlowCommoniOS (~> 1.12.0) - GBDeviceInfo (~> 6.6.0) - - GrowingTextView (~> 0.7.2) + - Introspect (~> 0.1) - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - MatomoTracker (~> 7.4.1) @@ -156,9 +156,9 @@ SPEC REPOS: - FLEX - FlowCommoniOS - GBDeviceInfo - - GrowingTextView - GZIP - HPGrowingTextView + - Introspect - JitsiMeetSDK - KeychainAccess - KituraContracts @@ -188,7 +188,7 @@ SPEC CHECKSUMS: AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce BlueCryptor: b0aee3d9b8f367b49b30de11cda90e1735571c24 BlueECC: 0d18e93347d3ec6d41416de21c1ffa4d4cd3c2cc - BlueRSA: 6f9776d62d9773502415a7db3bcbb2bbb3f71fc3 + BlueRSA: dfeef51db96bcc4edec654956c1581adbda4e6a3 DGCollectionViewLeftAlignFlowLayout: a0fa58797373ded039cafba8133e79373d048399 Down: b6ba1bc985c9d2f4e15e3b293d2207766fa12612 DSWaveformImage: 3c718a0cf99291887ee70d1d0c18d80101d3d9ce @@ -196,11 +196,11 @@ SPEC CHECKSUMS: DTFoundation: a53f8cda2489208cbc71c648be177f902ee17536 ffmpeg-kit-ios-audio: 8c44d93054e1a9743a7014ec3dd26cd1ad8f2a59 FLEX: e51461dd6f0bfb00643c262acdfea5d5d12c596b - FlowCommoniOS: e9ecbc97fb9ce5c593fb3da0e1073b65a3902026 + FlowCommoniOS: ca92071ab526dc89905495a37844fd7e78d1a7f2 GBDeviceInfo: ed0db16230d2fa280e1cbb39a5a7f60f6946aaec - GrowingTextView: 876bf42005b5e4a4fd740597db12caaf41f0fe6c GZIP: 416858efbe66b41b206895ac6dfd5493200d95b3 HPGrowingTextView: 88a716d97fb853bcb08a4a08e4727da17efc9b19 + Introspect: 2be020f30f084ada52bb4387fff83fa52c5c400e JitsiMeetSDK: 2f118fa770f23e518f3560fc224fae3ac7062223 KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51 KituraContracts: e845e60dc8627ad0a76fa55ef20a45451d8f830b @@ -218,7 +218,7 @@ SPEC CHECKSUMS: Reusable: 6bae6a5e8aa793c9c441db0213c863a64bce9136 SideMenu: f583187d21c5b1dd04c72002be544b555a2627a2 SwiftBase32: 9399c25a80666dc66b51e10076bf591e3bbb8f17 - SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 + SwiftGen: a6d22010845f08fe18fbdf3a07a8e380fd22e0ea SwiftJWT: 88c412708f58c169d431d344c87bc79a87c830ae SwiftLint: e96c0a8c770c7ebbc4d36c55baf9096bb65c4584 SwiftyBeaver: 84069991dd5dca07d7069100985badaca7f0ce82 @@ -226,6 +226,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 2740772a9b2d32e17876526875dfc58f67240ba0 +PODFILE CHECKSUM: 56547a5087a419eb4461b0fb59380381194392dc COCOAPODS: 1.11.2 diff --git a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme index 84ecb908a..a9bea1d96 100644 --- a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme +++ b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme @@ -4,7 +4,8 @@ version = "1.3"> + buildImplicitDependencies = "YES" + runPostActionsOnFailure = "NO"> Fx9i0A3EOw>w z)(eaxjxH>!Y=KOb+Z#R#)-idcFSNTe|2OLz&Zpn!Uw3>N?Wk7w&hW@GF-CsTGvYa! z2V;*)-DKGlnj`QfJ%=qO>t^Y!1#{-g&EjI=c=DLx-#O9FE{>XHpsfs^u6{1-oD!M< DZ%IRm literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_poll.imageset/action_poll@2x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_poll.imageset/action_poll@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8880642457b1a3e236a66bb7bd9895b894acfa19 GIT binary patch literal 245 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC&H|6fVg?2=RS;(M3{v?36l5$8 za(7}_cTVOdki(Mh=GY bFvRg6pD!kOBO-D)&{Yhcu6{1-oD!Mr&0DvkyOC){ z=R6Gt4vh!a30sY-^yj@e&6jqJp&?ZEm|tD~^~;|>TAV*4*Y~*Q|L^@hvNFg0(jMuFu8}$pShsL{!Q0s?}AXiH_;!CVkl3{5|2JA vX8kyP?*iR2w|E`w=3r<8Tgt((?;5vumR8EO?2cBT7a2TV{an^LB{Ts5XmDk} literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Polls/Contents.json b/Riot/Assets/Images.xcassets/Room/Polls/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Polls/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_delete_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Polls/poll_delete_icon.imageset/Contents.json new file mode 100644 index 000000000..19dac30ff --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Polls/poll_delete_icon.imageset/Contents.json @@ -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 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_delete_icon.imageset/poll_delete_icon.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_delete_icon.imageset/poll_delete_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e2ad145460bbd3ad36e04019b089a4c6966fb35f GIT binary patch literal 341 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjoCO|{#S9GG!XV7ZFl!D-1!HlL zyA#8@b22Z19F}xPUq=Rpjs4tz5?O)#SDr48Ar*{or`n!l3>0a*Z~W}H(4*}}LYzzc zg4$(oG>Jwu2c@$ov6onRot|;%{L)XxN?ewHN`h%^Jre`ZUcNK?^S$bMk3`rxB&x)v z+5%I$e4a#JoVm%fCR1anPlaeo@y~5CXREoLS|wgyzkhRz*t5gGU#3~U-y>|WFJ$8#=k)YRWPRZN$g%&J;gj15U-!kl{&4O?&8Dd6gK{o!S|gOMDhCM954`mM-(vS{ z`)%yA_9~p^{=mHWP1rR7gM|trElmLrf;TvCV48P`okecPRoUMUGGx;}x)>>6*RoOQ ju3K%m%S*Q^@2BVucbnIaE7#cpea+zM>gTe~DWM4fUa*Kb literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_delete_icon.imageset/poll_delete_icon@2x.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_delete_icon.imageset/poll_delete_icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b45ce02ed6f5e9d3271e1926f87da17a1022f99e GIT binary patch literal 581 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC&H|6fVg?2=RS;(M3{v?36l5$8 za(7}_cTVOdki(Mh=M~gUNFz zqx%8r0_Mk@J>KDu&dn`gDtxK?fu*QrQiF)O&z}wfkNNA@#dtiQTF0aK*?X4Wul!s0 zYf99NSq)yeI(YbbP4yPJAjDDWH*eDYyr1V7%Nz0SeA!J=JY!GU*u+HBhEZuJ?zYHFIww0~*$1*WwR zuBO;z{Z_4ESry!PBI~-LlKSI^jqwTWW>QmD9r0pp%u|W)G~f{FKh<;Jb-$mC`jwrk zdu#3&w%Oh&yC!v_%k^{D{yhCSjVwo_6FFEO+g-9hs@T9=E^E3Sbv%`o_+btGCJOqs`vyP7QWP{M$EM{};cuU&vT9edXfA x%dcFT8!Q+Z;x}f*BH0>;F*aVZoe$6kNso$Kk0M17}|4`7y;G7oG1U1Rm?*lU?oMYdXw>Hn3A3T#F z%(UOi_jrEd$*P4J*XjkOo_#7ZVqPk??n?H?)OmH!yp$YS_b|6UytYn~h5yu!xet@g z1dJc$oI1RD^Tw|wb<>?T--x|tK2L7uk)LM-W?stGx%lVv{6GhNxvy0_EdKwMR4w2$ z>1Mv?&U<$K?q6w@p9JU4@!M!{=T7pX zpaoID&lJeS&$oXvpVYS}_e>huUpxGl+m>X)roQj}_YHg|o|FD?bY0}R=bvre>v&{C z9s99IXYUDoW}G*N>8!kAS782PqqzBxZMp^h*-h(|55M?w{0FbsgRf_@w(18KIiGod zxnxiKq>8o8pPX;}S-$P8AFqVQTgNE%o*%zB4}W^}bP0 Hl+XkK^XFW7 literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_delete_option_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Polls/poll_delete_option_icon.imageset/Contents.json new file mode 100644 index 000000000..29638e38a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Polls/poll_delete_option_icon.imageset/Contents.json @@ -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 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_delete_option_icon.imageset/poll_delete_option_icon.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_delete_option_icon.imageset/poll_delete_option_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0a1dc0a326db65609476253fa9a4867ed3389349 GIT binary patch literal 630 zcmV-+0*U>JP)8e(B5g^m`|%L`E1sI5OZriow}Cm`o8 zCK_Wd#4&zNF8A|&x4XBy2obn4&7A%=u|>lbL@Y@uXL%k4bmb6kL<*Z)Hr*`hdRxVT zVUwn9mtMR5DicE-NZH8bwf!)m9|HI0^A_FIpd04k7a*c80@mhcrs{$DFWf#_27L_p z8*1}2g_`#xyaC@l0dj_M|2X3M5@4URTOc7wkOp&p3cPllz2EvgS^#qHBLQ?@%z4Ut_xFn$G>J-9qst6vB=}yo$br8^LDDgDxBV+3<~fIuzNl2__VQTJ$JMq zIj zC4AuQQ8B4NJhV8Do?FHW_=Ox8l)2*4#szL9qeGPaZi2RKt6yJQDk>kyJNCdj$GD}l Q6951J07*qoM6N<$f&-)sW&i*H literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_delete_option_icon.imageset/poll_delete_option_icon@2x.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_delete_option_icon.imageset/poll_delete_option_icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..bfc0e272d3cb18510907adc56b99b00fef97e3dd GIT binary patch literal 1142 zcmV-+1d02JP)7%C?z6kLJ1hI3Q@taDcD5T(BSf544IpK`99X zkO&YKWEup;J%o^5##okr($u4D`}>qsC=kd(n-H)G8XZu*^ZYbc{Yb+A(o~@U zAPgrI=8bQ-Z5Uha8SpL33Xu|!**lCoZg?Y!vEeHTt2bK`kSi9$C;9RcKaD^aV8F7y zL0{ZTZ?qJ!lRKINrY&^%tvA38%ku&Md@MZprp0~cpv&+>94?m&UCLB|ON|e85JkW3xt0K(%|^5Ag*fyPL58W!ao4IQgsccqfevNNc&3Sdthtp z^U%hJPh&o>SG^mX+MSBt*zeZ!7~~2{`SD~Zl-GNpbgW9RCO@V&N6O{0QanEPt!=y? z-{b~aeHM*>&7Tp^zvV(e0%I@e0yI^70+QfAH(py6A&fV+`FSsZJo-zg`n+#rk#>Lj zPNu%Cm564>Be!EwJGnp%2Q#(fKFCz``u@Y2d*ABY%|w_PKRYx1{Z{nmFR%e-fmGAf z39yM(5uGp|LEAlqk+#Om=?$e(6N?-Q4;zR4>8| zCyYn1P(YnpI+(drJx&>~!&s1lgR~OisPS+X4;&SM5d2!xzYzuwLwS`U4-R2A+X!SM zwiPHa;)sA&#tWPjp$}s2Noi=4i{^QmU?&TfDm@yn!4UfuM$19UEKbO-VvR7u>#US2 z+h7x`#>*}{tb(@F0+iOl2C~b8PKfY>lA@Rt@beT%g84vwTdR5AOoVJcP$eA-1+pRz zNDHXX2doCEW+Lq51B935@u&+ZD&?eXJivUQzpw8UQ$pDYD@2Ir?c@XKK6tE6KQ1dJ zUJTz$;6>kOcv@RZ*y)AKqQ~P~7UwkqIGA}wE(z`@frD3EG(@l;dxtBeOB)%oVC20P z9(-hMYgmo;D&7S*x)RocdOf~|G1?$N?;cHi0WJe~YmBZVXqpd<4qwG8o#lZ~-;~O8{+dD}2@X4f^&(|Zp(G4AR zDSc}dAnEu$8drD zT+%>IOhujGiFSm4!p8BU4@iQ=1D4h_mc^gj6!uH&c8xaj7rl8*HgPT7H2?qr07*qo IM6N<$f_w%5tN;K2 literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_delete_option_icon.imageset/poll_delete_option_icon@3x.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_delete_option_icon.imageset/poll_delete_option_icon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a209b1b37bb66b85967b4d854268d60397a56bbd GIT binary patch literal 1693 zcmV;O24eY%P)<<)LSg!Kf1F!N zT#}_Yh5<_xAaf@|{yCXeB}E`Fg{j6AYVh3Y8IVBJ0>_BTe844uav`q~Q zBwUKYl-F%dqm;%|HwGh(xm-g6J<06EdDs0NbTAai^xNlNpLKe@V}aH-vI+hx0o}oj z(_W4`xd<%>xoYe6^&>~?Vw9+pJ#UG2_W3bdBl9&Ps#vP|$@ zaeQZW&Qc&^(5uj66m@nTo0aP0#{|OhPA|AtHRx)Xk*GdiA&}^`2pNMOh4)zTYAdP` z=y|R%-iy{_!T>_mR#ef=KYqG%Tb4jezuk-6f3W-(evV!k+Prf4(i%ua)%`K3&1;3P z&2In5f*xo7ACo1{-G!y|$v^%aC=?1B{ybP-d2{~G-64>~4?ixvTYdkf-|t000%%1c zJ#f^_90|m9V;m%rTK)Tt>SL=9ADxvU5{k28asT^&9a@tN3=oqI0wGXoxUG8jp=puq z#8E)ZY^;Sq`+K@naX$KR6|Ga-;n_d-7Z^yBpK1^a)vQIYD2 zgU`D1N13Em|oKf@_p0 zCFB&ekb3TBN2|p#pUd^SIb zHHplBThOqfy?`l^^W}8vOGR9tTHFT}L?V@dIRLyFiOlCK- z&kDCXt-8=DxL`A=`6sFy=5lK0r16VkMN zPx=ChdJVD_u4Y#8L3w)cO(zT{#nu!%#R)g!AWd&>B3#a(SKDrL6K*v=AGzM_j#bk3 z55im$)|z=Ol-Pm-V0JfKm>n4jH=VkXy1^2*P0#hhxEDgn7N+dJY7pgBhb0cmx9Gxr zdT#SzUZyZ6$~SovJs#*WSz?teC8bi7s^_&9QThNRirWNoSBzo-8?Lmq+9vSu6H6He nOXD_y0U*iE&Hc1$QObD%A$hZ>Ro=x900000NkvXXu0mjfhlmib literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_edit_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Polls/poll_edit_icon.imageset/Contents.json new file mode 100644 index 000000000..b83445a6b --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Polls/poll_edit_icon.imageset/Contents.json @@ -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 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_edit_icon.imageset/poll_edit_icon.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_edit_icon.imageset/poll_edit_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9bcc9356e115cf14d34761bf77187cab51406f30 GIT binary patch literal 461 zcmV;;0W$uHP)wd1!@Ii1!4tY2Ur0mfF0z4$e}MhIO8~@S@&?%u!N9{ zd4lgH$!720yWPD7*n6rC@48m^#)nMWe}sRa#gU-<{Y4?t=ebfUO!`<*5Z?90pST8n zLc1^h3I(XR$8CP}k~TtEAbkYV*EmHL*KJxcj-@@e==3FuCem(n0>^QDBpgXQ+k!N4 zjSU9kga;$eyXY3WQffNbv@X{Pe#CVllXebvGoMAhthHNfkR{GL?`~V4nIe68xb=qo z>K74T`eyy)Ue|>?Srw;yrJ@exWMd7g4Yx1FnMV_Gjh!yO9046E{{HrQ28N{9fS@G| zdd{>K3>cD{A}&l*h_4N?ysVu-w`Hv7N(>XO=mU!ni;4q?E@EvTN`m(kt^}ynk8U{~ zmrxRfG1zq8s!eQN>9-At#73#D5^kN#0yo4 zN~PlIER;rsINq_>Hc|Y&n4bY=yt}iX4M0grN#X45AH3e(^S^nX7l0543PvZ71T6pz z2;GmhnqQZE9-%;Pl3!uWuN^7Xzcl%d!V1{kJ9x#1^QiZcoTgIWFCYdN3ZUjcKSrXr z_XZ&~wzju_Tu**}zAr5IHBj^Yx)X+NvuF%rBOpg~mkFK97oOu)gYeGqs(%z0l-mgC zhOpA%he3R_ zJb6u~Yv9SVt@nI*5f?|OH!peen#>B=+4tY^;bmN$PM(kwC9lb(fQ2Gp(+&v9qNtHC*>#9AATle3i{<>dLyx4;5c3ea+9>Nhr% zpMM{uM9Ud3SR+MX1HDjc&;*O5DnKM>1eZhAKLpDpUc_QKn@(cQG6{*=XqIv&MLdPF zmNO}$5y(T%q=?o4%bE6@Oyx|9aRHv^H~G*oiA^JCQj7~AM57x{yeFKbkuxchH4r;> zY{;1uYa?Jw&ZHO>Pz{1AYe3|NW!rBg7)_z4(?4~(YsVn!@lNuF{U-M_zr?73nM-57 za$=o*x2MXxIDp&XSd(xYg>W`0N=Tu=e{-qykR|n&cf~cA8Q35g=etguy6hU>*>xe tq|K86k73i`*}*6H18kAT=ILEWQ=i9~iH7A)nq5GJfo8O)RNJ&XaNl8gbn3=iq&dl8WuTp7Y4GfZj zM*xZWfC4uW;2EG;c~K}p_v-Qx4B)(=P!4S0|BOT}nUdtRpoiq#VM>y-f?mzezb6Tw zhMglNCJ2sEESIa)kPm2SO#_dk z-gRgBg5X@_R0Wkvr3zr?yMmnl|caf2X$(#R%0CuU|j_b%U2_EOatKt7{aPaT3?Q+ z_pZB(+-Z3j)+{j@K0#9sia*OH_h5^d6QO4gMlP%OtKN0OKz6XBB#ZL}K=b^zs&NRu{^ugK2UdGnMVKDms&f=q?a zvu5KnjDfLMkPYFJ%UCOj!l%qviH1-1?}RZS!zVql)i_q-t31!dyPR~%vyWOMlBVqB2z;ggHeChbD_dj}|XbJL!sDcUt{G`Gs7cD_Fx2Om@?%i}&!4Y(u^h?&+7KBeOniYhM3bH3( zaZnZ%#tJdw)a<4liADuwE2Eqx^OdV|B+5OALS^-#&45q{`T)Tp9gohMr#?80vLJc$ zB%?0`0<0kh$ALHHw;Oe;zb+*yBI=lVO+1FzN-c zr?=1J3Zfp|!fLxcxM3G5NX|Kdm;Isj^%VD@C-C5Yc;p-uU}k&5B_+j8{sQFtw)|#DMB literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_end_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Polls/poll_end_icon.imageset/Contents.json new file mode 100644 index 000000000..165d80098 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Polls/poll_end_icon.imageset/Contents.json @@ -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 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_end_icon.imageset/poll_end_icon.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_end_icon.imageset/poll_end_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8ca40ab42ed11516c32f5cd653fa3733b637f471 GIT binary patch literal 414 zcmV;P0b%}$P)Ivis9zjo_H&Acj))m|Uy@4hYhFwjYrY>}xhtL)n zLR-3l@U1#CFW=vJGXt<^r)X$|I6hC$Sx^!}h<4Sk4-`T#9a2gF$rZ950Jdq2_|urJ zRbWGw*&v5009x>zaKKg`qE@1UV7kqQmBi ztIO}K3J!!gy)qTkJw$o4K>S>*vh+7o;p5%2t)|c3w zOdz;5o4-)I5;>eD@?jS;uyn{mw7PmH57VKi+io&Ye6KMf zf$9I!)?~g7Vcu)_esmZf07xVfiA3^`NkM`smd_fw?D2g%ozs`gg(m7@kWh*hwa++f zZC3cxd^YWF_bHG-cI63nE9yOfgi)+ipBabNZiVMX{fW8*fASqs4cB|3^~3~lkblv% z=|EV61mG+`=~!JGdw?MFW)H+jPKAvln6@EP1mZBIdMQeZYsVsODAPyapnq zpq@7s!x&hUd-K`k5j+xE06Tfc`756DE%1RbKpr_tXS!It#+EVTtG1?TWAMqo|*PAjkDCKR4UcL0|iyRM9aQK`7NkC zAq;@GMHj4k3PJKX5|8j?5lxXFKi^+%2mx^b-1D?;1Qk~yT*%+(2!w#;DMXSaOVYbl z>(4}#@`UX%>NSLYfB5o*?e77SBS|;g$vNz*5w^T(;ikz+&QRb^F<|I*yM=-4Z(<5z zD2MNOcIUx0`;{kL|D+@dybE>Cn9FUje|f_5EL2cyc*?9t_IDy0dBWE)XCERudBXQJ zDh?5?JmLGl(d9zdGTD<)9MrDxeaOcl9~yW-)j%SVNF)-8cYXm0jyYpC>liWs0000< KMNUMnLSTZu#tmNp literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_end_icon.imageset/poll_end_icon@3x.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_end_icon.imageset/poll_end_icon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..282514b329dfbd2df438875577e325bb51ca9c8b GIT binary patch literal 959 zcmV;w13>(VP)Vt%hAi(v2%4FFxeknfU>w}x`;1$gLGW`wF1PT4rfao2N^i0e2NKkgZY6{H|1!nRrL=Y?Eis&W8$O3cS$_av#9Z7+mJ{WHq2wVa$^`?-OAcaO7jp%9$w7?T2ceva$CF3m(wD`iAt!nc7{ZX8MOiH4 zd_M@9n~(xV2pX51=y{+y~EMM^^yzaz+jnYo%6 zld?@|Acv!~;A9mtKq}~Z*o|+!!~ex&#(NtoL+|^dEC=9+7@MxdjZH^Ey2J*1j$2>x zI^M_n02!c*93)l{PUi03kTG8r&(tN&%+0^mMGg`x$nG=QA74*X(m)RED1_!B2Zqsx+K)I%Z}G+w?9Jybe4lC?}J{<&SN)Qk;RE~@4fTi%`H$scR7gi zPWo!1w86#xM;3=22-~zQ&ICC~qIWUZ_ucYU?@%PkmRsyM$rL$=v<3}>McaA)?7;4a zYeJG27mGD(vRZXJm+h~^_f3+6Boc%qkt8%NSEi<*)4uqIzfIR(y|y@4IFHgp4s!2t zN0PZPXRyPW)v&^FPa(Etg}o&mgOVNf%0W4zcCG$tHdQvLxiIp;EGAP9mW h2!bF8f*^>@`41s_%TS$pm3sgH002ovPDHLkV1g@Xv)ljx literal 0 HcmV?d00001 diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 0ff5e437d..c8b57ab05 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -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"; diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index cd4a5b92e..92e82c765 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -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") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index f3622c608..3ddc20669 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -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") diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 9e1ccd05b..4b372d460 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -165,7 +165,7 @@ final class RiotSettings: NSObject { @UserDefault(key: "roomScreenAllowFilesAction", defaultValue: BuildSettings.roomScreenAllowFilesAction, storage: defaults) var roomScreenAllowFilesAction - + @UserDefault(key: "roomScreenShowsURLPreviews", defaultValue: true, storage: defaults) var roomScreenShowsURLPreviews diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index 4764e03e9..fa9a5d2db 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -29,6 +29,8 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { private let roomViewController: RoomViewController private let activityIndicatorPresenter: ActivityIndicatorPresenterType private var selectedEventId: String? + + private var pollEditFormCoordinator: PollEditFormCoordinator? private var roomDataSourceManager: MXKRoomDataSourceManager { return MXKRoomDataSourceManager.sharedManager(forMatrixSession: self.parameters.session) @@ -240,4 +242,16 @@ extension RoomCoordinator: RoomViewControllerDelegate { func roomViewController(_ roomViewController: RoomViewController, handleUniversalLinkWith parameters: UniversalLinkParameters) -> Bool { return AppDelegate.theDelegate().handleUniversalLink(with: parameters) } + + func roomViewControllerDidRequestPollCreationFormPresentation(_ roomViewController: RoomViewController) { + guard #available(iOS 14.0, *) else { + return + } + + let parameters = PollEditFormCoordinatorParameters(navigationRouter: self.navigationRouter) + + pollEditFormCoordinator = PollEditFormCoordinator(parameters: parameters) + + pollEditFormCoordinator?.start() + } } diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index 5016dec0f..3723eb5d8 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -176,6 +176,13 @@ extern NSNotificationName const RoomGroupCallTileTappedNotification; - (BOOL)roomViewController:(RoomViewController *)roomViewController handleUniversalLinkWithParameters:(UniversalLinkParameters*)parameters; +/** + Ask the coordinator to invoke the poll creation form coordinator. + + @param roomViewController the `RoomViewController` instance. + */ +- (void)roomViewControllerDidRequestPollCreationFormPresentation:(RoomViewController *)roomViewController; + @end NS_ASSUME_NONNULL_END diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index eba7a4ba4..589b6be8c 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -1987,16 +1987,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; RoomInputToolbarView *roomInputView = ((RoomInputToolbarView *) self.inputToolbarView); MXWeakify(self); NSMutableArray *actionItems = [NSMutableArray new]; - if (RiotSettings.shared.roomScreenAllowCameraAction) - { - [actionItems addObject:[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_camera"] andAction:^{ - MXStrongifyAndReturnIfNil(self); - if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) { - ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO; - } - [self showCameraControllerAnimated:YES]; - }]]; - } if (RiotSettings.shared.roomScreenAllowMediaLibraryAction) { [actionItems addObject:[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_media_library"] andAction:^{ @@ -2027,6 +2017,26 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self roomInputToolbarViewDidTapFileUpload]; }]]; } + if (BuildSettings.roomScreenAllowPollsAction) + { + [actionItems addObject:[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_poll"] andAction:^{ + MXStrongifyAndReturnIfNil(self); + if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) { + ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO; + } + [self.delegate roomViewControllerDidRequestPollCreationFormPresentation:self]; + }]]; + } + if (RiotSettings.shared.roomScreenAllowCameraAction) + { + [actionItems addObject:[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_camera"] andAction:^{ + MXStrongifyAndReturnIfNil(self); + if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) { + ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO; + } + [self showCameraControllerAnimated:YES]; + }]]; + } roomInputView.actionsBar.actionItems = actionItems; } diff --git a/RiotSwiftUI/Debug.xcconfig b/RiotSwiftUI/Debug.xcconfig index 11a7288a4..d890f7e35 100644 --- a/RiotSwiftUI/Debug.xcconfig +++ b/RiotSwiftUI/Debug.xcconfig @@ -18,3 +18,4 @@ // https://help.apple.com/xcode/#/dev745c5c974 #include "Common.xcconfig" +#include "Pods/Target Support Files/Pods-RiotPods-RiotSwiftUI/Pods-RiotPods-RiotSwiftUI.debug.xcconfig" diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index 1195cd4ff..51c3e182c 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -23,7 +23,8 @@ enum MockAppScreens { MockTemplateUserProfileScreenState.self, MockTemplateRoomListScreenState.self, MockTemplateRoomChatScreenState.self, - MockUserSuggestionScreenState.self + MockUserSuggestionScreenState.self, + MockPollEditFormScreenState.self ] } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/BorderedInputFieldStyle.swift b/RiotSwiftUI/Modules/Common/Util/BorderedInputFieldStyle.swift similarity index 81% rename from RiotSwiftUI/Modules/Settings/Notifications/View/BorderedInputFieldStyle.swift rename to RiotSwiftUI/Modules/Common/Util/BorderedInputFieldStyle.swift index 89e4349fd..5fbcac5e6 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/BorderedInputFieldStyle.swift +++ b/RiotSwiftUI/Modules/Common/Util/BorderedInputFieldStyle.swift @@ -16,6 +16,7 @@ import Foundation import SwiftUI +import Introspect @available(iOS 14.0, *) /// A bordered style of text input @@ -49,10 +50,11 @@ struct BorderedInputFieldStyle: TextFieldStyle { } private var textColor: Color { - if !isEnabled { - return theme.colors.quarterlyContent + if (theme.identifier == ThemeIdentifier.dark) { + return (isEnabled ? theme.colors.primaryContent : theme.colors.tertiaryContent) + } else { + return (isEnabled ? theme.colors.primaryContent : theme.colors.quarterlyContent) } - return theme.colors.primaryContent } private var backgroundColor: Color { @@ -62,21 +64,31 @@ struct BorderedInputFieldStyle: TextFieldStyle { return theme.colors.background } + private var placeholderColor: Color { + return theme.colors.tertiaryContent + } + private var borderWidth: CGFloat { - return isEditing || isError ? 2 : 1.5 + return isEditing || isError ? 2.0 : 1.5 } func _body(configuration: TextField<_Label>) -> some View { - let rect = RoundedRectangle(cornerRadius: 8) + let rect = RoundedRectangle(cornerRadius: 8.0) return configuration .font(theme.fonts.callout) .foregroundColor(textColor) .accentColor(accentColor) - .frame(height: 48) - .padding(.horizontal, 8) + .frame(height: 48.0) + .padding(.horizontal, 8.0) .background(backgroundColor) .clipShape(rect) .overlay(rect.stroke(borderColor, lineWidth: borderWidth)) + .introspectTextField { textField in + textField.returnKeyType = .done + textField.clearButtonMode = .whileEditing + textField.attributedPlaceholder = NSAttributedString(string: textField.placeholder ?? "", + attributes: [NSAttributedString.Key.foregroundColor: UIColor(placeholderColor)]) + } } } @@ -118,6 +130,5 @@ struct BorderedInputFieldStyle_Previews: PreviewProvider { .padding() .theme(ThemeIdentifier.dark) } - } } diff --git a/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift b/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift new file mode 100644 index 000000000..dd710f70e --- /dev/null +++ b/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift @@ -0,0 +1,207 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@available(iOS 14.0, *) +struct MultilineTextField: View { + + @Environment(\.theme) private var theme: ThemeSwiftUI + + @Binding private var text: String + @State private var dynamicHeight: CGFloat = 100 + @State private var isEditing = false + + private var placeholder: String = "" + + private var showingPlaceholder: Bool { + text.isEmpty + } + + init(_ placeholder: String, text: Binding) { + self.placeholder = placeholder + self._text = text + } + + private var textColor: Color { + if (theme.identifier == ThemeIdentifier.dark) { + return theme.colors.primaryContent + } else { + return theme.colors.primaryContent + } + } + + private var backgroundColor: Color { + return theme.colors.background + } + + private var placeholderColor: Color { + return theme.colors.tertiaryContent + } + + private var borderColor: Color { + if isEditing { + return theme.colors.accent + } + + return theme.colors.quarterlyContent + } + + private var borderWidth: CGFloat { + return isEditing ? 2.0 : 1.5 + } + + var body: some View { + let rect = RoundedRectangle(cornerRadius: 8.0) + return UITextViewWrapper(text: $text, calculatedHeight: $dynamicHeight, isEditing: $isEditing) + .frame(minHeight: dynamicHeight, maxHeight: dynamicHeight) + .padding(4.0) + .background(placeholderView, alignment: .topLeading) + .animation(.none) + .background(backgroundColor) + .clipShape(rect) + .overlay(rect.stroke(borderColor, lineWidth: borderWidth)) + .introspectTextView { textView in + textView.textColor = UIColor(textColor) + textView.font = theme.fonts.uiFonts.callout + } + } + + @ViewBuilder + private var placeholderView: some View { + if showingPlaceholder { + Text(placeholder) + .foregroundColor(placeholderColor) + .font(theme.fonts.callout) + .padding(.leading, 8.0) + .padding(.top, 12.0) + } + } +} + +@available(iOS 14.0, *) +fileprivate struct UITextViewWrapper: UIViewRepresentable { + typealias UIViewType = UITextView + + @Binding var text: String + @Binding var calculatedHeight: CGFloat + @Binding var isEditing: Bool + + func makeUIView(context: UIViewRepresentableContext) -> UITextView { + let textView = UITextView() + textView.delegate = context.coordinator + + textView.isEditable = true + textView.font = UIFont.preferredFont(forTextStyle: .body) + textView.isSelectable = true + textView.isUserInteractionEnabled = true + textView.isScrollEnabled = false + textView.backgroundColor = UIColor.clear + textView.returnKeyType = .done + + textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + return textView + } + + func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext) { + if uiView.text != self.text { + uiView.text = self.text + } + + UITextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight) + } + + fileprivate static func recalculateHeight(view: UIView, result: Binding) { + let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude)) + if result.wrappedValue != newSize.height { + DispatchQueue.main.async { + result.wrappedValue = newSize.height // !! must be called asynchronously + } + } + } + + func makeCoordinator() -> Coordinator { + return Coordinator(text: $text, height: $calculatedHeight, isEditing: $isEditing) + } + + final class Coordinator: NSObject, UITextViewDelegate { + var text: Binding + var calculatedHeight: Binding + var isEditing: Binding + + init(text: Binding, height: Binding, isEditing: Binding) { + self.text = text + self.calculatedHeight = height + self.isEditing = isEditing + } + + func textViewDidChange(_ uiView: UITextView) { + text.wrappedValue = uiView.text + UITextViewWrapper.recalculateHeight(view: uiView, result: calculatedHeight) + } + + func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + if text == "\n" { + textView.resignFirstResponder() + return false + } + + return true + } + + func textViewDidBeginEditing(_ textView: UITextView) { + isEditing.wrappedValue = true + } + + func textViewDidEndEditing(_ textView: UITextView) { + isEditing.wrappedValue = false + } + } +} + +@available(iOS 14.0, *) +struct MultilineTextField_Previews: PreviewProvider { + + static var previews: some View { + return Group { + VStack { + PreviewWrapper() + PlaceholderPreviewWrapper() + PreviewWrapper() + .theme(ThemeIdentifier.dark) + PlaceholderPreviewWrapper() + .theme(ThemeIdentifier.dark) + } + } + .padding() + } + + struct PreviewWrapper: View { + @State(initialValue: "123") var text: String + + var body: some View { + MultilineTextField("Placeholder", text: $text) + } + } + + struct PlaceholderPreviewWrapper: View { + @State(initialValue: "") var text: String + + var body: some View { + MultilineTextField("Placeholder", text: $text) + } + } +} diff --git a/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift b/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift new file mode 100644 index 000000000..9d94bac8d --- /dev/null +++ b/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift @@ -0,0 +1,52 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@available(iOS 14.0, *) +struct PrimaryActionButtonStyle: ButtonStyle { + @Environment(\.theme) private var theme: ThemeSwiftUI + + var enabled: Bool = false + + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + .padding(12.0) + .frame(maxWidth: .infinity) + .foregroundColor(.white) + .font(theme.fonts.body) + .background(configuration.isPressed ? theme.colors.accent.opacity(0.6) : theme.colors.accent) + .opacity(enabled ? 1.0 : 0.6) + .cornerRadius(8.0) + } +} + +@available(iOS 14.0, *) +struct PrimaryActionButtonStyle_Previews: PreviewProvider { + static var previews: some View { + Group { + VStack { + Button("Enabled") { } + .buttonStyle(PrimaryActionButtonStyle(enabled: true)) + + Button("Disabled") { } + .buttonStyle(PrimaryActionButtonStyle(enabled: false)) + .disabled(true) + } + .padding() + } + } +} diff --git a/RiotSwiftUI/Modules/Common/Util/SafeBindingCollectionEnumerator.swift b/RiotSwiftUI/Modules/Common/Util/SafeBindingCollectionEnumerator.swift new file mode 100644 index 000000000..45ea51855 --- /dev/null +++ b/RiotSwiftUI/Modules/Common/Util/SafeBindingCollectionEnumerator.swift @@ -0,0 +1,40 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +/** + Used to avoid crashes when enumerating through bindings in the AnswerOptions ForEach + https://stackoverflow.com/q/65375372 + Replace with Swift 5.5 bindings enumerator later. + */ +@available(iOS 14.0, *) +struct SafeBindingCollectionEnumerator: View { + + typealias BoundElement = Binding + private let binding: BoundElement + private let content: (BoundElement) -> C + + init(_ binding: Binding, index: T.Index, @ViewBuilder content: @escaping (BoundElement) -> C) { + self.content = content + self.binding = .init(get: { binding.wrappedValue[index] }, + set: { binding.wrappedValue[index] = $0 }) + } + + var body: some View { + content(binding) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift new file mode 100644 index 000000000..1df71686e --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift @@ -0,0 +1,81 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/PollEditForm PollEditForm +/* + Copyright 2021 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation +import UIKit +import SwiftUI + +struct PollEditFormCoordinatorParameters { + let navigationRouter: NavigationRouterType? +} + +final class PollEditFormCoordinator: Coordinator { + + // MARK: - Properties + + // MARK: Private + + private let parameters: PollEditFormCoordinatorParameters + private let pollEditFormHostingController: UIViewController + private var _pollEditFormViewModel: Any? = nil + + @available(iOS 14.0, *) + fileprivate var pollEditFormViewModel: PollEditFormViewModel { + return _pollEditFormViewModel as! PollEditFormViewModel + } + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + + // MARK: - Setup + + @available(iOS 14.0, *) + init(parameters: PollEditFormCoordinatorParameters) { + self.parameters = parameters + + let viewModel = PollEditFormViewModel() + let view = PollEditForm(viewModel: viewModel.context) + + _pollEditFormViewModel = viewModel + pollEditFormHostingController = VectorHostingController(rootView: view) + } + + // MARK: - Public + func start() { + guard #available(iOS 14.0, *) else { + MXLog.debug("[PollEditFormCoordinator] start: Invalid iOS version, returning.") + return + } + + MXLog.debug("[PollEditFormCoordinator] did start.") + + parameters.navigationRouter?.present(pollEditFormHostingController, animated: true) + + pollEditFormViewModel.completion = { [weak self] result in + guard let self = self else { return } + switch result { + case .cancel: + self.parameters.navigationRouter?.dismissModule(animated: true, completion: nil) + case .create(_, _): + break + } + } + } +} diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift new file mode 100644 index 000000000..9e6657506 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift @@ -0,0 +1,77 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/PollEditForm PollEditForm +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +enum PollEditFormStateAction { + case viewAction(PollEditFormViewAction) +} + +enum PollEditFormViewAction { + case addAnswerOption + case deleteAnswerOption(PollEditFormAnswerOption) + case cancel + case create +} + +enum PollEditFormViewModelResult { + case cancel + case create(String, [String]) +} + +struct PollEditFormQuestion { + var text: String { + didSet { + text = String(text.prefix(maxLength)) + } + } + + let maxLength: Int +} + +struct PollEditFormAnswerOption: Identifiable, Equatable { + let id = UUID() + + var text: String { + didSet { + text = String(text.prefix(maxLength)) + } + } + + let maxLength: Int +} + +struct PollEditFormViewState: BindableState { + let maxAnswerOptionsCount: Int + var bindings: PollEditFormViewStateBindings + + var confirmationButtonEnabled: Bool { + !bindings.question.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && + bindings.answerOptions.filter({ !$0.text.isEmpty }).count >= 2 + } + + var addAnswerOptionButtonEnabled: Bool { + bindings.answerOptions.count < maxAnswerOptionsCount + } +} + +struct PollEditFormViewStateBindings { + var question: PollEditFormQuestion + var answerOptions: [PollEditFormAnswerOption] +} diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift new file mode 100644 index 000000000..2e545a9d4 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift @@ -0,0 +1,34 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/UserSuggestion UserSuggestion +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +@available(iOS 14.0, *) +enum MockPollEditFormScreenState: MockScreenState, CaseIterable { + case standard + + var screenType: Any.Type { + MockPollEditFormScreenState.self + } + + var screenView: ([Any], AnyView) { + let viewModel = PollEditFormViewModel() + return ([viewModel], AnyView(PollEditForm(viewModel: viewModel.context))) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift new file mode 100644 index 000000000..b77274bcb --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift @@ -0,0 +1,91 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/PollEditForm PollEditForm +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import Combine + +@available(iOS 14, *) +typealias PollEditFormViewModelType = StateStoreViewModel< PollEditFormViewState, + PollEditFormStateAction, + PollEditFormViewAction > +@available(iOS 14, *) +class PollEditFormViewModel: PollEditFormViewModelType { + + private struct Constants { + static let maxAnswerOptionsCount = 20 + static let maxQuestionLength = 200 + static let maxAnswerOptionLength = 200 + } + + // MARK: - Properties + + // MARK: Private + + // MARK: Public + + var completion: ((PollEditFormViewModelResult) -> Void)? + + // MARK: - Setup + + init() { + super.init(initialViewState: Self.defaultState()) + } + + private static func defaultState() -> PollEditFormViewState { + return PollEditFormViewState( + maxAnswerOptionsCount: Constants.maxAnswerOptionsCount, + bindings: PollEditFormViewStateBindings( + question: PollEditFormQuestion(text: "", maxLength: Constants.maxQuestionLength), + answerOptions: [PollEditFormAnswerOption(text: "", maxLength: Constants.maxAnswerOptionLength), + PollEditFormAnswerOption(text: "", maxLength: Constants.maxAnswerOptionLength) + ] + ) + ) + } + + // MARK: - Public + + override func process(viewAction: PollEditFormViewAction) { + switch viewAction { + case .cancel: + completion?(.cancel) + case .create: + completion?(.create(state.bindings.question.text.trimmingCharacters(in: .whitespacesAndNewlines), + state.bindings.answerOptions.compactMap({ answerOption in + let text = answerOption.text.trimmingCharacters(in: .whitespacesAndNewlines) + return text.isEmpty ? nil : text + }))) + default: + dispatch(action: .viewAction(viewAction)) + } + } + + override class func reducer(state: inout PollEditFormViewState, action: PollEditFormStateAction) { + switch action { + case .viewAction(let viewAction): + switch viewAction { + case .deleteAnswerOption(let answerOption): + state.bindings.answerOptions.removeAll { $0 == answerOption } + case .addAnswerOption: + state.bindings.answerOptions.append(PollEditFormAnswerOption(text: "", maxLength: Constants.maxAnswerOptionLength)) + default: + break + } + } + } +} diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift new file mode 100644 index 000000000..daee82cbd --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift @@ -0,0 +1,82 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/PollEditForm PollEditForm +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import RiotSwiftUI + +@available(iOS 14.0, *) +class PollEditFormUITests: XCTestCase { + + private var app: XCUIApplication! + + override func setUp() { + continueAfterFailure = false + + app = XCUIApplication() + app.launch() + app.buttons[MockPollEditFormScreenState.screenStateKeys.first!].tap() + } + + func testInitialStateComponents() { + + XCTAssert(app.scrollViews.firstMatch.exists) + + XCTAssert(app.staticTexts["Create poll"].exists) + XCTAssert(app.staticTexts["Poll question or topic"].exists) + XCTAssert(app.staticTexts["Question or topic"].exists) + XCTAssert(app.staticTexts["Create options"].exists) + + XCTAssert(app.textViews.count == 1) + + XCTAssert(app.textFields.count == 2) + XCTAssert(app.staticTexts["Option 1"].exists) + XCTAssert(app.staticTexts["Option 2"].exists) + + let cancelButton = app.buttons["Cancel"] + XCTAssert(cancelButton.exists) + XCTAssertTrue(cancelButton.isEnabled) + + let addOptionButton = app.buttons["Add option"] + XCTAssert(addOptionButton.exists) + XCTAssertTrue(addOptionButton.isEnabled) + + let createPollButton = app.buttons["Create poll"] + XCTAssert(createPollButton.exists) + XCTAssertFalse(createPollButton.isEnabled) + } + + func testRemoveAddAnswerOptions() { + + let deleteAnswerOptionButton = app.buttons["Delete answer option"].firstMatch + + XCTAssert(deleteAnswerOptionButton.waitForExistence(timeout: 2.0)) + deleteAnswerOptionButton.tap() + + XCTAssert(deleteAnswerOptionButton.waitForExistence(timeout: 2.0)) + deleteAnswerOptionButton.tap() + + let addOptionButton = app.buttons["Add option"] + XCTAssert(addOptionButton.waitForExistence(timeout: 2.0)) + XCTAssertTrue(addOptionButton.isEnabled) + + for i in 1...3 { + addOptionButton.tap() + XCTAssert(app.staticTexts["Option \(i)"].waitForExistence(timeout: 2.0)) + } + } +} diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Test/Unit/PollEditFormViewModelTests.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Test/Unit/PollEditFormViewModelTests.swift new file mode 100644 index 000000000..739361197 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Test/Unit/PollEditFormViewModelTests.swift @@ -0,0 +1,123 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/PollEditForm PollEditForm +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Combine + +@testable import RiotSwiftUI + +@available(iOS 14.0, *) +class PollEditFormViewModelTests: XCTestCase { + var viewModel: PollEditFormViewModel! + var context: PollEditFormViewModelType.Context! + var cancellables = Set() + + override func setUpWithError() throws { + viewModel = PollEditFormViewModel() + context = viewModel.context + } + + func testInitialState() { + XCTAssertTrue(context.question.text.isEmpty) + XCTAssertFalse(context.viewState.confirmationButtonEnabled) + XCTAssertTrue(context.viewState.addAnswerOptionButtonEnabled) + + XCTAssertEqual(context.answerOptions.count, 2) + for answerOption in context.answerOptions { + XCTAssertTrue(answerOption.text.isEmpty) + } + } + + func testDeleteAllAnswerOptions() { + while !context.answerOptions.isEmpty { + context.send(viewAction: .deleteAnswerOption(context.answerOptions.first!)) + } + + XCTAssertEqual(context.answerOptions.count, 0) + XCTAssertFalse(context.viewState.confirmationButtonEnabled) + XCTAssertTrue(context.viewState.addAnswerOptionButtonEnabled) + } + + func testAddRemoveAnswerOption() { + context.send(viewAction: .addAnswerOption) + + XCTAssertEqual(context.answerOptions.count, 3) + + context.send(viewAction: .deleteAnswerOption(context.answerOptions.first!)) + + XCTAssertEqual(context.answerOptions.count, 2) + } + + func testCreateEnabled() { + context.question.text = "Some question" + context.answerOptions[0].text = "First answer" + context.answerOptions[1].text = "Second answer" + + XCTAssertTrue(context.viewState.confirmationButtonEnabled) + } + + func testReachedMaxAnswerOptions() { + for _ in 0...context.viewState.maxAnswerOptionsCount { + context.send(viewAction: .addAnswerOption) + } + + XCTAssertFalse(context.viewState.addAnswerOptionButtonEnabled) + } + + func testQuestionMaxLength() { + let question = String(repeating: "S", count: context.question.maxLength + 100) + context.question.text = question + + XCTAssertEqual(context.question.text.count, context.question.maxLength) + } + + func testAnswerOptionMaxLength() { + let answerOption = String(repeating: "S", count: context.answerOptions[0].maxLength + 100) + context.answerOptions[0].text = answerOption + + XCTAssertEqual(context.answerOptions[0].text.count, context.answerOptions[0].maxLength) + } + + func testFormCompletion() { + let question = "Some question " + let firstAnswer = "First answer " + let secondAnswer = "Second answer " + let thirdAnswer = " " + + viewModel.completion = { result in + if case PollEditFormViewModelResult.create(let resultQuestion, let resultAnswerOptions) = result { + XCTAssertEqual(question.trimmingCharacters(in: .whitespacesAndNewlines), resultQuestion) + + // The last answer option should be automatically dropped as it's empty + XCTAssertEqual(resultAnswerOptions.count, 2) + + XCTAssertEqual(resultAnswerOptions[0], firstAnswer.trimmingCharacters(in: .whitespacesAndNewlines)) + XCTAssertEqual(resultAnswerOptions[1], secondAnswer.trimmingCharacters(in: .whitespacesAndNewlines)) + } + } + + context.question.text = question + context.answerOptions[0].text = firstAnswer + context.answerOptions[1].text = secondAnswer + + context.send(viewAction: .addAnswerOption) + context.answerOptions[2].text = thirdAnswer + + context.send(viewAction: .create) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift new file mode 100644 index 000000000..1d874688b --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift @@ -0,0 +1,146 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh Room/PollEditForm PollEditForm +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@available(iOS 14.0, *) +struct PollEditForm: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + @ObservedObject var viewModel: PollEditFormViewModel.Context + + var body: some View { + NavigationView { + GeometryReader { proxy in + ScrollView { + VStack(alignment: .leading, spacing: 32.0) { + + VStack(alignment: .leading, spacing: 16.0) { + Text(VectorL10n.pollEditFormPollQuestionOrTopic) + .font(theme.fonts.title3SB) + .foregroundColor(theme.colors.primaryContent) + + VStack(alignment: .leading, spacing: 8.0) { + Text(VectorL10n.pollEditFormQuestionOrTopic) + .font(theme.fonts.subheadline) + .foregroundColor(theme.colors.primaryContent) + + MultilineTextField(VectorL10n.pollEditFormInputPlaceholder, text: $viewModel.question.text) + } + } + + VStack(alignment: .leading, spacing: 16.0) { + Text(VectorL10n.pollEditFormCreateOptions) + .font(theme.fonts.title3SB) + .foregroundColor(theme.colors.primaryContent) + + ForEach(0.. Void + + var body: some View { + VStack(alignment: .leading, spacing: 8.0) { + Text(VectorL10n.pollEditFormOptionNumber(index + 1)) + .font(theme.fonts.subheadline) + .foregroundColor(theme.colors.primaryContent) + + HStack(spacing: 16.0) { + TextField(VectorL10n.pollEditFormInputPlaceholder, text: $text, onEditingChanged: { edit in + self.focused = edit + }) + .textFieldStyle(BorderedInputFieldStyle(theme: _theme, isEditing: focused)) + Button { + onDelete() + } label: { + Image(uiImage:Asset.Images.pollDeleteOptionIcon.image) + } + .accessibilityIdentifier("Delete answer option") + } + } + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct PollEditForm_Previews: PreviewProvider { + static let stateRenderer = MockPollEditFormScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup() + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift index 6e4fab347..1491aa579 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift @@ -61,14 +61,20 @@ class UserSuggestionService: UserSuggestionServiceProtocol { // MARK: - Setup - init(roomMemberProvider: RoomMembersProviderProtocol) { + init(roomMemberProvider: RoomMembersProviderProtocol, shouldDebounce: Bool = true) { self.roomMemberProvider = roomMemberProvider - currentTextTriggerSubject - .debounce(for: 0.5, scheduler: RunLoop.main) - .removeDuplicates() - .sink { [weak self] in self?.fetchAndFilterMembersForTextTrigger($0) } - .store(in: &cancellables) + if (shouldDebounce) { + currentTextTriggerSubject + .debounce(for: 0.5, scheduler: RunLoop.main) + .removeDuplicates() + .sink { [weak self] in self?.fetchAndFilterMembersForTextTrigger($0) } + .store(in: &cancellables) + } else { + currentTextTriggerSubject + .sink { [weak self] in self?.fetchAndFilterMembersForTextTrigger($0) } + .store(in: &cancellables) + } } // MARK: - UserSuggestionServiceProtocol diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift index 1c34f7eb3..fce852059 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift @@ -27,7 +27,7 @@ class UserSuggestionServiceTests: XCTestCase { var service: UserSuggestionService? override func setUp() { - service = UserSuggestionService(roomMembersProvider: self) + service = UserSuggestionService(roomMemberProvider: self, shouldDebounce: false) } func testAlice() { @@ -116,7 +116,7 @@ extension UserSuggestionServiceTests: RoomMembersProviderProtocol { ("Bob", "@bob:matrix.org")] members(users.map({ user in - RoomMembersProviderMember(identifier: user.1, displayName: user.0, avatarURL: "") + RoomMembersProviderMember(userId: user.1, displayName: user.0, avatarUrl: "") })) } } diff --git a/RiotSwiftUI/Release.xcconfig b/RiotSwiftUI/Release.xcconfig index 11a7288a4..561518704 100644 --- a/RiotSwiftUI/Release.xcconfig +++ b/RiotSwiftUI/Release.xcconfig @@ -18,3 +18,4 @@ // https://help.apple.com/xcode/#/dev745c5c974 #include "Common.xcconfig" +#include "Pods/Target Support Files/Pods-RiotPods-RiotSwiftUI/Pods-RiotPods-RiotSwiftUI.release.xcconfig" diff --git a/RiotSwiftUI/RiotSwiftUIApp.swift b/RiotSwiftUI/RiotSwiftUIApp.swift index 63dbece0d..902f7327d 100644 --- a/RiotSwiftUI/RiotSwiftUIApp.swift +++ b/RiotSwiftUI/RiotSwiftUIApp.swift @@ -21,6 +21,14 @@ import SwiftUI struct RiotSwiftUIApp: App { init() { UILog.configure(logger: PrintLogger.self) + + switch UITraitCollection.current.userInterfaceStyle { + case .dark: + ThemePublisher.configure(themeId: .dark) + default: + ThemePublisher.configure(themeId: .light) + } + } var body: some Scene { WindowGroup {