Polls refinements (#1608)

* Refine focus behavior

* Begin draft mode

* Add alert on poll form

* Add poll ended asset

* Add fallback text for ended poll event

* Cleanup

* Fix assets

* Remove poll feature flags

* Fix UI tests

* Fix ui tests

* Refine discard poll alert

* Remove unused import

* Rename hasDraftContent -> hasContent

* Restore createPoll-2 ref screenshots
This commit is contained in:
Alfonso Grillo
2023-09-01 15:55:11 +02:00
committed by GitHub
parent 41662a5313
commit 9b313b814e
36 changed files with 458 additions and 89 deletions
@@ -66,13 +66,11 @@ struct RoomAttachmentPicker: View {
}
.accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPickerLocation)
if ServiceLocator.shared.settings.pollsCreationEnabled {
Button {
context.showAttachmentPopover = false
context.send(viewAction: .displayPollForm)
} label: {
PickerLabel(title: L10n.screenRoomAttachmentSourcePoll, icon: Image(asset: Asset.Images.timelinePollAttachment))
}
Button {
context.showAttachmentPopover = false
context.send(viewAction: .displayPollForm)
} label: {
PickerLabel(title: L10n.screenRoomAttachmentSourcePoll, icon: Image(asset: Asset.Images.timelinePollAttachment))
}
}
.padding(.top, isPresented ? 20 : 0)
@@ -39,6 +39,12 @@ struct CreatePollScreenViewStateBindings {
var isCreateButtonDisabled: Bool {
question.isEmpty || options.count < 2 || options.contains { $0.text.isEmpty }
}
var hasContent: Bool {
!question.isEmpty || options.contains(where: { !$0.text.isEmpty }) || isUndisclosed
}
var alertInfo: AlertInfo<UUID>?
}
enum CreatePollScreenViewAction {
@@ -39,7 +39,15 @@ class CreatePollScreenViewModel: CreatePollScreenViewModelType, CreatePollScreen
options: state.bindings.options.map(\.text),
pollKind: state.bindings.isUndisclosed ? .undisclosed : .disclosed))
case .cancel:
actionsSubject.send(.cancel)
if state.bindings.hasContent {
state.bindings.alertInfo = .init(id: .init(),
title: L10n.screenCreatePollDiscardConfirmationTitle,
message: L10n.screenCreatePollDiscardConfirmation,
primaryButton: .init(title: L10n.actionCancel, action: nil),
secondaryButton: .init(title: L10n.actionOk, action: { self.actionsSubject.send(.cancel) }))
} else {
actionsSubject.send(.cancel)
}
case .deleteOption(let index):
// fixes a crash that caused an index out of range when an option with the keyboard focus was deleted
Task {
@@ -39,6 +39,8 @@ struct CreatePollScreen: View {
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
.animation(.elementDefault, value: context.options)
.interactiveDismissDisabled(context.viewState.bindings.hasContent)
.alert(item: $context.alertInfo)
}
// MARK: - Private
@@ -55,6 +57,10 @@ struct CreatePollScreen: View {
.textFieldStyle(.compoundForm)
.focused($focus, equals: .question)
.accessibilityIdentifier(A11yIdentifiers.createPollScreen.question)
.onSubmit {
focus = context.options.indices.first.map { .option(index: $0) }
}
.submitLabel(.next)
}
.compoundFormSection()
}
@@ -74,6 +80,11 @@ struct CreatePollScreen: View {
}
.focused($focus, equals: .option(index: index))
.accessibilityIdentifier(A11yIdentifiers.createPollScreen.optionID(index))
.onSubmit {
let nextOptionIndex = index == context.options.endIndex - 1 ? nil : index + 1
focus = nextOptionIndex.map { .option(index: $0) }
}
.submitLabel(index == context.options.endIndex - 1 ? .done : .next)
}
}
.onMove { offsets, toOffset in
@@ -83,6 +94,7 @@ struct CreatePollScreen: View {
if context.options.count < context.viewState.maxNumberOfOptions {
Button(L10n.screenCreatePollAddOptionBtn) {
context.send(viewAction: .addOption)
focus = context.options.indices.last.map { .option(index: $0) }
}
.accessibilityIdentifier(A11yIdentifiers.createPollScreen.addOption)
}
@@ -814,7 +814,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
private func endPoll(pollStartID: String) {
Task {
let endPollResult = await roomProxy.endPoll(pollStartID: pollStartID)
let endPollResult = await roomProxy.endPoll(pollStartID: pollStartID,
text: "The poll with event id: \(pollStartID) has ended")
switch endPollResult {
case .success:
break
@@ -21,6 +21,7 @@ struct PollRoomTimelineView: View {
@Environment(\.timelineStyle) var timelineStyle
@EnvironmentObject private var context: RoomScreenViewModel.Context
@ScaledMetric private var summaryPadding = 32
@ScaledMetric private var iconSize = 22
var body: some View {
TimelineStyler(timelineItem: timelineItem) {
@@ -58,7 +59,11 @@ struct PollRoomTimelineView: View {
private var questionView: some View {
HStack(alignment: .top, spacing: 12) {
Image(Asset.Images.timelinePoll.name)
let asset = poll.hasEnded ? Asset.Images.timelineEndedPoll : Asset.Images.timelinePoll
Image(asset.name)
.resizable()
.frame(width: iconSize, height: iconSize)
Text(poll.question)
.multilineTextAlignment(.leading)
@@ -97,19 +97,31 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
}
/// The action's label.
@ViewBuilder
var label: some View {
switch self {
case .copy: return Label(L10n.actionCopy, systemImage: "doc.on.doc")
case .edit: return Label(L10n.actionEdit, systemImage: "pencil.line")
case .copyPermalink: return Label(L10n.actionCopyLinkToMessage, systemImage: "link")
case .reply: return Label(L10n.actionReply, systemImage: "arrowshape.turn.up.left")
case .forward: return Label(L10n.actionForward, systemImage: "arrowshape.turn.up.right")
case .redact: return Label(L10n.actionRemove, systemImage: "trash")
case .viewSource: return Label(L10n.actionViewSource, systemImage: "doc.text.below.ecg")
case .retryDecryption: return Label(L10n.actionRetryDecryption, systemImage: "arrow.down.message")
case .report: return Label(L10n.actionReportContent, systemImage: "exclamationmark.bubble")
case .react: return Label(L10n.actionReact, systemImage: "hand.thumbsup")
case .endPoll: return Label { Text(L10n.actionEndPoll) } icon: { Image.compound.check.resizable() }
case .copy:
Label(L10n.actionCopy, systemImage: "doc.on.doc")
case .edit:
Label(L10n.actionEdit, systemImage: "pencil.line")
case .copyPermalink:
Label(L10n.actionCopyLinkToMessage, systemImage: "link")
case .reply:
Label(L10n.actionReply, systemImage: "arrowshape.turn.up.left")
case .forward:
Label(L10n.actionForward, systemImage: "arrowshape.turn.up.right")
case .redact:
Label(L10n.actionRemove, systemImage: "trash")
case .viewSource:
Label(L10n.actionViewSource, systemImage: "doc.text.below.ecg")
case .retryDecryption:
Label(L10n.actionRetryDecryption, systemImage: "arrow.down.message")
case .report:
Label(L10n.actionReportContent, systemImage: "exclamationmark.bubble")
case .react:
Label(L10n.actionReact, systemImage: "hand.thumbsup")
case .endPoll:
Label { Text(L10n.actionEndPoll) } icon: { CompoundIcon(customImage: Asset.Images.endedPoll.swiftUIImage) }
}
}
}
@@ -50,9 +50,7 @@ protocol DeveloperOptionsProtocol: AnyObject {
var readReceiptsEnabled: Bool { get set }
var notificationSettingsEnabled: Bool { get set }
var swiftUITimelineEnabled: Bool { get set }
var pollsInTimelineEnabled: Bool { get set }
var richTextEditorEnabled: Bool { get set }
var pollsCreationEnabled: Bool { get set }
}
extension AppSettings: DeveloperOptionsProtocol { }
@@ -60,16 +60,6 @@ struct DeveloperOptionsScreen: View {
}
}
Section("Polls") {
Toggle(isOn: $context.pollsInTimelineEnabled) {
Text("View polls in timeline")
}
Toggle(isOn: $context.pollsCreationEnabled) {
Text("View polls creation flow")
}
}
Section("Rich Text Editor") {
Toggle(isOn: $context.richTextEditorEnabled) {
Text("Use the Rich Text Editor")