Skip to content

Commit c4a0c62

Browse files
nuno-vieiraStream-SDK-BotStream Bot
committed
[Polls] Add Poll Creation Flow (#3433)
* [Polls] Add `PollAttachmentView` + `ViewContainerBuilder` (#3374) * Introduce new ViewContainerBuilder * Add PollAttachmentView component * Change to SwiftUI ENV * Add `shouldShowOnlineIndicator` to turn off online presence in `ChatUserAvatarView` * Fix small iOS 13 deprecation * Add domain logic helpers to Poll * Message Polls live updates * Event.ownVotes workaround (Drop when fixed) * Manage data races when voting on a poll in the message list * Add haptic feedback when voting * Make sure to update the latestVotes author avatar views * Improve performance of Option List View by no recreating the item views everytime * Improve flexibility of Option List View without compromising performance * Make content of StackedUserAvatarsView public * Re-structure Common Buttons Folder * Add `CheckboxButton` as a common view * Add vote ratio logic to `ChatChannelListItemView` * Fix UI Glitches for option list item view when text is too big and avatar appears and disappears * Extract more common logic to `Poll` domain model * Add closed poll and winner option state logic * Add subtitle text logic to `PollAttachmentView` * Change winner logic to be the one and only one option with most votes * Add channel list message preview text for polls * Add poll quoted message text rendering * Add View Results and End Button to the Poll Attachment View * Fix poll option label width ui glitch * Added End Poll functionality * Add isOptionWinner and isOptionOneOfTheWinners to Poll * Fix message with Polls not showing when long pressing * Fix poll controller with incorrect data in message list * Sort the `latestVotes` and `latestAnswers` in the LLC * Add test coverage to Poll domain helpers * Add Test Coverage to Poll Message Preview in Channel List * Add test coverage to PollAttachmentView * Add test coverage to disabling online indicator in ChatUserAvatarView * Add test coverage to quoted poll * Fix some existing snapshot tests * Use UIStackView for Spacer() to avoid drawing * Fix Xcode 14 Build * Fix Poll AttachmentView Tests on CI * Update CHANGELOG.md * Update CHANGELOG.md * Fix Xcode 14 Build, conflict with SwiftUI Spacer * Revert "Change to SwiftUI ENV" This reverts commit 6d9129a. * Add documentation to ViewContainerBuilder * Fix vale issues * Fix using pin() instead of constraint() in docs * [Polls] Add `PollResultsVC` (#3381) * [Polls] Comments + Suggestions + Anonymous Polls + LLC Fixes (#3398) * Refactor PollResultsVC Section views to be more simple and flat hierarchy * Fix bottom spacing in Poll Results Section Header * Add comment buttons to `PollAttachmentView` * Add comment to poll implementation * Fix `poll.latestAnswers` being reset after poll event did not retreive the `latestAnswers` data We should not treat optional `latestAnswers` as empty, since they have different meanings * Add `PollCommentListVC` implementation * Fix `PollVoteListQueryDTO.filterHash` using filterHash only instead of queryHash This was causing the pollId to not be considered when fetching the DB votes * Make diffable data sources more stable * Fix first page of poll vote list resetting all votes * Fix the default vote list sorting * Add poll suggestions feature * Add anonymous polls support * Fix AlertsRouter docs * Add test coverage to `PollCommentListVC` * Add test coverage when poll comments are 0 * Add test coverage to allow suggestions * Add test coverage to anonymous polls * Re-record snapshots after renaming test file * Fix snapshot on CI * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update Sources/StreamChatUI/ChatMessageList/Attachments/Poll/PollAttachmentView.swift * Update Sources/StreamChatUI/ChatMessageList/Attachments/Poll/PollCommentListVC/PollCommentListSectionFooterView.swift * Fix comments and suggestion buttons showing when poll is closed * Update CHANGELOG.md * Change alert titles to follow Apples guideline * Improve comments view spacing * Optimize a bit to check if a user has already a comment * Add poll creation action to composer if polls are enabled * Add Poll Creation Views and UX * Handle keyboard adjustment in the poll creation view * Add actual if-statement support to ViewContainerBuilder * Add Poll Creation View Styling * Add poll creation section header view * Add Poll Creation Feature Cell Styling * Add Multiple Votes Feature Cell Data handling * Improve data handling in PollCreationVC * Add poll creation button * Fix Poll Creation Memory Leaks + Add Poll Option Errors * Implement poll option error indices * Refactor Poll Creation View to Collection View * Add views to components and appearance * Implement keyboard management in Poll Creation VC * Implementation of discarding poll changes * Add poll alert error if creation fails * Fix basic features not being persisted * Add localization to the views * Add for polls feature support configuration * Add Poll Creation View test coverage * Update CHANGELOG.md * Fix test_canCreatePoll_whenOptionsError_shouldReturnFalse test * [CI] Snapshots (#3434) Co-authored-by: Stream Bot <[email protected]> * Fix maximum votes error not being cleared when multiple votes is disabled * Change multiple votes keyboard to number bad * Remove custom equatable implementation that is not required for PollFeatureType * Remove yeetd-normal.pkg file? * Fix adding duplicate options * Disable swiftlint notification_center_detachment and switch_case_alignment * Extract poll creation input view height to a constant --------- Co-authored-by: Stream SDK Bot <[email protected]> Co-authored-by: Stream Bot <[email protected]>
1 parent ac53bd0 commit c4a0c62

File tree

50 files changed

+2005
-27
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2005
-27
lines changed

.swiftlint.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ disabled_rules:
3131
- type_body_length
3232
- opening_brace
3333
- line_length
34+
- switch_case_alignment
35+
- notification_center_detachment
3436

3537
# TODO: https://github.com/GetStream/ios-issues-tracking/issues/538
3638
- attributes # it should be included in `opt_in_rules`

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
2626
- Add `PollAttachmentView` component to render polls in the message list [#3374](https://github.com/GetStream/stream-chat-swift/pull/3374)
2727
- Add `PollResultsVC` component to show the results of a poll [#3381](https://github.com/GetStream/stream-chat-swift/pull/3381)
2828
- Add `PollCommentListVC` component to show the comments of a poll [#3398](https://github.com/GetStream/stream-chat-swift/pull/3398)
29+
- Add `PollCreationVC` component to create a poll in a channel [#3433](https://github.com/GetStream/stream-chat-swift/pull/3433)
2930
- Add `ChatUserAvatarView.shouldShowOnlineIndicator` to disable the online indicator easily [#3374](https://github.com/GetStream/stream-chat-swift/pull/3374)
3031
### 🎭 New Localizations
3132
Multiple localizations were added to Polls, for more details please check the strings file.

Sources/StreamChatUI/Appearance+ColorPalette.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public extension Appearance {
6060

6161
// MARK: - Tint and alert
6262

63+
public var validationError: UIColor = .streamAccentRed
6364
public var alert: UIColor = .streamAccentRed
6465
public var alternativeActiveTint: UIColor = .streamAccentGreen
6566
public var inactiveTint: UIColor = .streamGray

Sources/StreamChatUI/Appearance+Images.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ public extension Appearance {
8181

8282
// MARK: - Polls
8383

84+
public var pollReorderIcon: UIImage = loadSafely(systemName: "line.3.horizontal", assetsFallback: "line.3.horizontal")
85+
public var pollCreationSendIcon: UIImage = loadSafely(systemName: "paperplane.fill", assetsFallback: "paperplane.fill")
8486
public var pollWinner: UIImage = loadSafely(systemName: "trophy", assetsFallback: "trophy")
8587
public var pollVoteCheckmarkActive: UIImage = .checkmark
8688
public var pollVoteCheckmarkInactive: UIImage = UIImage(
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//
2+
// Copyright © 2024 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import UIKit
6+
7+
/// The cell for enabling or disabling a poll feature.
8+
open class PollCreationFeatureCell: _CollectionViewCell, ThemeProvider {
9+
public struct Content {
10+
public var feature: PollFeature
11+
12+
public init(feature: PollFeature) {
13+
self.feature = feature
14+
}
15+
}
16+
17+
public var content: Content? {
18+
didSet {
19+
updateContentIfNeeded()
20+
}
21+
}
22+
23+
/// The main container that holds the subviews.
24+
open private(set) lazy var container = HContainer()
25+
26+
/// A view that displays the feature name and the switch to enable/disable the feature.
27+
open private(set) lazy var featureSwitchView = components
28+
.pollCreationFeatureSwitchView.init()
29+
.withoutAutoresizingMaskConstraints
30+
31+
/// A closure that is triggered whenever the switch value changes.
32+
public var onValueChange: ((Bool) -> Void)?
33+
34+
override open func setUp() {
35+
super.setUp()
36+
37+
contentView.isUserInteractionEnabled = true
38+
featureSwitchView.onValueChange = { [weak self] newValue in
39+
self?.onValueChange?(newValue)
40+
}
41+
}
42+
43+
override open func setUpAppearance() {
44+
super.setUpAppearance()
45+
46+
backgroundColor = appearance.colorPalette.background
47+
container.backgroundColor = appearance.colorPalette.background1
48+
container.layer.cornerRadius = 16
49+
}
50+
51+
override open func setUpLayout() {
52+
super.setUpLayout()
53+
54+
container.views {
55+
featureSwitchView
56+
}
57+
.height(PollCreationVC.pollCreationInputViewHeight)
58+
.layout {
59+
$0.isLayoutMarginsRelativeArrangement = true
60+
$0.directionalLayoutMargins = .init(top: 0, leading: 12, bottom: 0, trailing: 12)
61+
}
62+
.embed(in: self, insets: .init(top: 6, leading: 12, bottom: 6, trailing: 12))
63+
}
64+
65+
override open func updateContent() {
66+
super.updateContent()
67+
68+
guard let feature = content?.feature else {
69+
return
70+
}
71+
72+
featureSwitchView.switchView.isOn = feature.isEnabled
73+
featureSwitchView.featureNameLabel.text = feature.name
74+
}
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// Copyright © 2024 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import UIKit
6+
7+
/// A view responsible to enable or disable a poll feature.
8+
open class PollCreationFeatureSwitchView: _View, ThemeProvider {
9+
/// A label to show the name of the feature.
10+
open private(set) lazy var featureNameLabel = UILabel()
11+
.withoutAutoresizingMaskConstraints
12+
13+
/// A view to switch on or off. Used to enable or disable poll features.
14+
open private(set) lazy var switchView = UISwitch()
15+
.withoutAutoresizingMaskConstraints
16+
17+
/// A closure that is triggered whenever the switch value changes.
18+
public var onValueChange: ((Bool) -> Void)?
19+
20+
override open func setUp() {
21+
super.setUp()
22+
23+
switchView.addTarget(self, action: #selector(switchChangedValue(sender:)), for: .valueChanged)
24+
}
25+
26+
override open func setUpAppearance() {
27+
super.setUpAppearance()
28+
29+
featureNameLabel.font = appearance.fonts.body
30+
}
31+
32+
override open func setUpLayout() {
33+
super.setUpLayout()
34+
35+
HContainer(spacing: 4, alignment: .center) {
36+
featureNameLabel
37+
switchView
38+
}.embed(in: self)
39+
}
40+
41+
@objc open func switchChangedValue(sender: Any?) {
42+
onValueChange?(switchView.isOn)
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
//
2+
// Copyright © 2024 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import UIKit
6+
7+
/// The cell to configure the multiple votes poll feature.
8+
open class PollCreationMultipleVotesFeatureCell: _CollectionViewCell, ThemeProvider, UITextFieldDelegate {
9+
public struct Content {
10+
public var feature: MultipleVotesPollFeature
11+
public var maximumVotesErrorText: String?
12+
13+
public init(
14+
feature: MultipleVotesPollFeature,
15+
maximumVotesErrorText: String?
16+
) {
17+
self.feature = feature
18+
self.maximumVotesErrorText = maximumVotesErrorText
19+
}
20+
}
21+
22+
public var content: Content? {
23+
didSet {
24+
updateContentIfNeeded()
25+
}
26+
}
27+
28+
/// The main container that holds the subviews.
29+
open private(set) lazy var container = VContainer(spacing: 4)
30+
31+
/// A view that displays the feature name and the switch to enable/disable the feature.
32+
open private(set) lazy var featureSwitchView = components
33+
.pollCreationFeatureSwitchView.init()
34+
.withoutAutoresizingMaskConstraints
35+
36+
/// A view to configure the maximum votes per user.
37+
open private(set) lazy var maximumVotesSwitchView = PollCreationMaximumVotesSwitchView()
38+
.withoutAutoresizingMaskConstraints
39+
40+
private var currentMaximumVotesText: String = "" {
41+
didSet {
42+
validateMaximumVotesValue(currentMaximumVotesText)
43+
onMaximumVotesTextChanged?(currentMaximumVotesText)
44+
}
45+
}
46+
47+
/// A closure that is triggered whenever the feature is enabled or disabled.
48+
public var onFeatureEnabledChanged: ((Bool) -> Void)?
49+
50+
/// A closure that is triggered whenever the maximum votes value changes.
51+
public var onMaximumVotesValueChanged: ((Int?) -> Void)?
52+
53+
/// A closure that is triggered whenever the maximum votes text changes.
54+
public var onMaximumVotesTextChanged: ((String) -> Void)?
55+
56+
/// A closure that is triggered whenever the validation of the maximum votes changes.
57+
public var onMaximumVotesErrorTextChanged: ((String?) -> Void)?
58+
59+
/// The text for the maximum votes input placeholder.
60+
open var maximumVotesPlaceholderText: String {
61+
L10n.Polls.Creation.maximumVotesPlaceholder
62+
}
63+
64+
/// The error text for the maximum votes input.
65+
open var maximumVotesErrorText: String {
66+
L10n.Polls.Creation.maximumVotesError
67+
}
68+
69+
override open func setUp() {
70+
super.setUp()
71+
72+
contentView.isUserInteractionEnabled = true
73+
74+
maximumVotesSwitchView.textFieldView.inputTextField.delegate = self
75+
76+
featureSwitchView.onValueChange = { [weak self] isOn in
77+
self?.onFeatureEnabledChanged?(isOn)
78+
self?.resetMaximumVotesInput()
79+
self?.clearMaxVotesError()
80+
}
81+
82+
maximumVotesSwitchView.textFieldView.onTextChanged = { [weak self] _, newValue in
83+
self?.currentMaximumVotesText = newValue
84+
}
85+
86+
maximumVotesSwitchView.onValueChange = { [weak self] _ in
87+
self?.resetMaximumVotesInput()
88+
}
89+
}
90+
91+
override open func setUpAppearance() {
92+
super.setUpAppearance()
93+
94+
backgroundColor = appearance.colorPalette.background
95+
container.backgroundColor = appearance.colorPalette.background1
96+
container.layer.cornerRadius = 16
97+
}
98+
99+
override open func setUpLayout() {
100+
super.setUpLayout()
101+
102+
container.views {
103+
featureSwitchView
104+
.height(PollCreationVC.pollCreationInputViewHeight)
105+
maximumVotesSwitchView
106+
.height(PollCreationVC.pollCreationInputViewHeight)
107+
}
108+
.layout {
109+
$0.isLayoutMarginsRelativeArrangement = true
110+
$0.directionalLayoutMargins = .init(top: 0, leading: 12, bottom: 0, trailing: 12)
111+
}
112+
.embed(in: self, insets: .init(top: 6, leading: 12, bottom: 6, trailing: 12))
113+
}
114+
115+
override open func updateContent() {
116+
super.updateContent()
117+
118+
guard let content = self.content else {
119+
return
120+
}
121+
122+
featureSwitchView.featureNameLabel.text = content.feature.name
123+
featureSwitchView.switchView.isOn = content.feature.isEnabled
124+
maximumVotesSwitchView.isHidden = content.feature.maxVotesConfig == nil ? true : !content.feature.isEnabled
125+
maximumVotesSwitchView.switchView.isOn = content.feature.maxVotesConfig?.isEnabled ?? false
126+
maximumVotesSwitchView.textFieldView.content = .init(
127+
placeholder: maximumVotesPlaceholderText,
128+
errorText: content.maximumVotesErrorText
129+
)
130+
}
131+
132+
/// Sets the maximum votes in the maximum votes switch view.
133+
open func setMaximumVotesText(_ text: String) {
134+
maximumVotesSwitchView.textFieldView.setText(text)
135+
}
136+
137+
/// Validates the maximum votes text and shows an error if it is not valid.
138+
open func validateMaximumVotesValue(_ newValue: String) {
139+
let errorText = maximumVotesErrorText
140+
141+
if newValue.isEmpty && maximumVotesSwitchView.switchView.isOn {
142+
showMaxVotesError(message: errorText)
143+
return
144+
}
145+
146+
if newValue.isEmpty {
147+
clearMaxVotesError()
148+
maximumVotesSwitchView.switchView.setOn(false, animated: true)
149+
return
150+
}
151+
152+
maximumVotesSwitchView.switchView.setOn(true, animated: true)
153+
154+
guard let value = Int(newValue), value >= 1 && value <= 10 else {
155+
showMaxVotesError(message: errorText)
156+
return
157+
}
158+
159+
clearMaxVotesError()
160+
onMaximumVotesValueChanged?(value)
161+
}
162+
163+
/// Shows an error in the maximum votes switch view.
164+
open func showMaxVotesError(message: String) {
165+
maximumVotesSwitchView.textFieldView.content?.errorText = message
166+
onMaximumVotesErrorTextChanged?(message)
167+
}
168+
169+
/// Clears the error of the maximum votes switch view.
170+
open func clearMaxVotesError() {
171+
maximumVotesSwitchView.textFieldView.content?.errorText = nil
172+
onMaximumVotesErrorTextChanged?(nil)
173+
}
174+
175+
open func resetMaximumVotesInput() {
176+
maximumVotesSwitchView.textFieldView.inputTextField.text = nil
177+
currentMaximumVotesText = ""
178+
}
179+
180+
open func textFieldShouldReturn(_ textField: UITextField) -> Bool {
181+
textField.resignFirstResponder()
182+
return true
183+
}
184+
}
185+
186+
open class PollCreationMaximumVotesSwitchView: _View, ThemeProvider {
187+
/// A text field that supports showing validator errors.
188+
open private(set) lazy var textFieldView = components
189+
.pollCreationTextFieldView.init()
190+
.withoutAutoresizingMaskConstraints
191+
192+
/// A view to switch on or off. Used to enable or disable poll features.
193+
open private(set) lazy var switchView = UISwitch()
194+
.withoutAutoresizingMaskConstraints
195+
196+
/// A closure that is triggered whenever the switch value changes.
197+
public var onValueChange: ((Bool) -> Void)?
198+
199+
override open func setUp() {
200+
super.setUp()
201+
202+
textFieldView.inputTextField.keyboardType = .numberPad
203+
switchView.addTarget(self, action: #selector(switchChangedValue(sender:)), for: .valueChanged)
204+
}
205+
206+
override open func setUpLayout() {
207+
super.setUpLayout()
208+
209+
HContainer(spacing: 4, alignment: .center) {
210+
textFieldView
211+
switchView
212+
}
213+
.embed(in: self)
214+
}
215+
216+
@objc open func switchChangedValue(sender: Any?) {
217+
onValueChange?(switchView.isOn)
218+
}
219+
}

0 commit comments

Comments
 (0)