Edit poll UX (#2151)

* Add edit poll on room proxy

* Add CreatePollMode

* Add “edit poll” presentation flow

* Add delete poll section

* Inject editing poll

* Add submit action

* Refactor validation logic

* Add edit/delete actions

* Fix bubble timestamp for polls

* Update localisations

* Refactor CreatePoll -> PollForm

* Refactor tests

* Update rust sdk to 0.0.5-november23

* Update confirmation alerts

* Add edit support in TimelineItem menu

* Refactor a11y id

* Cleanup

* Fix failing tests

* Add tests

* Refine isEditable workaround

* Refactor timestamp in TimelineItemBubbledStylerView
This commit is contained in:
Alfonso Grillo
2023-11-23 12:19:15 +01:00
committed by GitHub
parent 4d011b9116
commit e583f52c65
36 changed files with 670 additions and 335 deletions
@@ -36,7 +36,7 @@ enum ComposerToolbarViewModelAction {
case displayMediaPicker
case displayDocumentPicker
case displayLocationPicker
case displayPollForm
case displayNewPollForm
case handlePasteOrDrop(provider: NSItemProvider)
@@ -55,7 +55,7 @@ enum ComposerToolbarViewAction {
case displayMediaPicker
case displayDocumentPicker
case displayLocationPicker
case displayPollForm
case displayNewPollForm
case handlePasteOrDrop(provider: NSItemProvider)
case enableTextFormatting
case composerAction(action: ComposerAction)
@@ -128,8 +128,8 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
actionsSubject.send(.displayDocumentPicker)
case .displayLocationPicker:
actionsSubject.send(.displayLocationPicker)
case .displayPollForm:
actionsSubject.send(.displayPollForm)
case .displayNewPollForm:
actionsSubject.send(.displayNewPollForm)
case .handlePasteOrDrop(let provider):
actionsSubject.send(.handlePasteOrDrop(provider: provider))
case .enableTextFormatting:
@@ -77,7 +77,7 @@ struct RoomAttachmentPicker: View {
Button {
context.showAttachmentPopover = false
context.send(viewAction: .displayPollForm)
context.send(viewAction: .displayNewPollForm)
} label: {
Label(L10n.screenRoomAttachmentSourcePoll, icon: \.polls)
.labelStyle(.menuSheet)
@@ -1,55 +0,0 @@
//
// Copyright 2022 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
enum CreatePollScreenViewModelAction {
case create(question: String, options: [String], pollKind: Poll.Kind)
case cancel
}
struct CreatePollScreenViewState: BindableState {
let maxNumberOfOptions = 20
var bindings: CreatePollScreenViewStateBindings = .init()
}
struct CreatePollScreenViewStateBindings {
var question = ""
var options: [Option] = [.init(), .init()]
var isUndisclosed = false
struct Option: Identifiable, Equatable {
let id = UUID()
var text = ""
}
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 {
case cancel
case create
case deleteOption(index: Int)
case addOption
}
@@ -17,27 +17,30 @@
import Combine
import SwiftUI
struct CreatePollScreenCoordinatorParameters { }
enum CreatePollScreenCoordinatorAction {
case cancel
case create(question: String, options: [String], pollKind: Poll.Kind)
struct PollFormScreenCoordinatorParameters {
let mode: PollFormMode
}
final class CreatePollScreenCoordinator: CoordinatorProtocol {
private let parameters: CreatePollScreenCoordinatorParameters
private var viewModel: CreatePollScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<CreatePollScreenCoordinatorAction, Never> = .init()
enum PollFormScreenCoordinatorAction {
case cancel
case delete
case submit(question: String, options: [String], pollKind: Poll.Kind)
}
final class PollFormScreenCoordinator: CoordinatorProtocol {
private let parameters: PollFormScreenCoordinatorParameters
private var viewModel: PollFormScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<PollFormScreenCoordinatorAction, Never> = .init()
private var cancellables = Set<AnyCancellable>()
var actions: AnyPublisher<CreatePollScreenCoordinatorAction, Never> {
var actions: AnyPublisher<PollFormScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: CreatePollScreenCoordinatorParameters) {
init(parameters: PollFormScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = CreatePollScreenViewModel()
viewModel = PollFormScreenViewModel(mode: parameters.mode)
}
func start() {
@@ -46,16 +49,18 @@ final class CreatePollScreenCoordinator: CoordinatorProtocol {
guard let self else { return }
switch action {
case let .create(question, options, pollKind):
self.actionsSubject.send(.create(question: question, options: options, pollKind: pollKind))
case .cancel:
self.actionsSubject.send(.cancel)
case .delete:
self.actionsSubject.send(.delete)
case let .submit(question, options, pollKind):
self.actionsSubject.send(.submit(question: question, options: options, pollKind: pollKind))
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(CreatePollScreen(context: viewModel.context))
AnyView(PollFormScreen(context: viewModel.context))
}
}
@@ -0,0 +1,122 @@
//
// Copyright 2022 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
enum PollFormScreenViewModelAction: Equatable {
case cancel
case delete
case submit(question: String, options: [String], pollKind: Poll.Kind)
}
struct PollFormScreenViewState: BindableState {
let mode: PollFormMode
let maxNumberOfOptions = 20
var bindings: PollFormScreenViewStateBindings = .init()
init(mode: PollFormMode) {
self.mode = mode
switch mode {
case .new:
bindings = .init()
case .edit(_, let poll):
bindings = .init(poll: poll)
}
}
var navigationTitle: String {
switch mode {
case .new:
return L10n.screenCreatePollTitle
case .edit:
return L10n.screenEditPollTitle
}
}
var submitButtonTitle: String {
switch mode {
case .new:
return L10n.actionCreate
case .edit:
return L10n.actionDone
}
}
var isSubmitButtonDisabled: Bool {
switch mode {
case .new:
return !bindings.hasValidContent
case .edit:
return !bindings.hasValidContent || !formContentHasChanged
}
}
var formContentHasChanged: Bool {
let initialBindings: PollFormScreenViewStateBindings
switch mode {
case .new:
initialBindings = .init()
case .edit(_, let poll):
initialBindings = .init(poll: poll)
}
return bindings != initialBindings
}
}
enum PollFormMode: Hashable {
case new
case edit(eventID: String, poll: Poll)
}
struct PollFormScreenViewStateBindings: Equatable {
var question = ""
var options: [Option] = [.init(), .init()]
var isUndisclosed = false
struct Option: Identifiable, Equatable {
let id = UUID()
var text = ""
}
var hasValidContent: Bool {
!question.isEmpty && options.count >= 2 && options.allSatisfy { !$0.text.isEmpty }
}
var alertInfo: AlertInfo<UUID>?
static func == (lhs: PollFormScreenViewStateBindings, rhs: PollFormScreenViewStateBindings) -> Bool {
lhs.question == rhs.question && lhs.options.map(\.text) == rhs.options.map(\.text) && lhs.isUndisclosed == rhs.isUndisclosed
}
}
extension PollFormScreenViewStateBindings {
init(poll: Poll) {
self.init(question: poll.question,
options: poll.options.map { .init(text: $0.text) },
isUndisclosed: poll.kind == .undisclosed)
}
}
enum PollFormScreenViewAction {
case cancel
case submit
case delete
case deleteOption(index: Int)
case addOption
}
@@ -17,29 +17,35 @@
import Combine
import SwiftUI
typealias CreatePollScreenViewModelType = StateStoreViewModel<CreatePollScreenViewState, CreatePollScreenViewAction>
typealias PollFormScreenViewModelType = StateStoreViewModel<PollFormScreenViewState, PollFormScreenViewAction>
class CreatePollScreenViewModel: CreatePollScreenViewModelType, CreatePollScreenViewModelProtocol {
private var actionsSubject: PassthroughSubject<CreatePollScreenViewModelAction, Never> = .init()
class PollFormScreenViewModel: PollFormScreenViewModelType, PollFormScreenViewModelProtocol {
private var actionsSubject: PassthroughSubject<PollFormScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<CreatePollScreenViewModelAction, Never> {
var actions: AnyPublisher<PollFormScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init() {
super.init(initialViewState: .init())
init(mode: PollFormMode) {
super.init(initialViewState: .init(mode: mode))
}
// MARK: - Public
override func process(viewAction: CreatePollScreenViewAction) {
override func process(viewAction: PollFormScreenViewAction) {
switch viewAction {
case .create:
actionsSubject.send(.create(question: state.bindings.question,
case .submit:
actionsSubject.send(.submit(question: state.bindings.question,
options: state.bindings.options.map(\.text),
pollKind: state.bindings.isUndisclosed ? .undisclosed : .disclosed))
case .delete:
state.bindings.alertInfo = .init(id: .init(),
title: L10n.screenEditPollDeleteConfirmationTitle,
message: L10n.screenEditPollDeleteConfirmation,
primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil),
secondaryButton: .init(title: L10n.actionOk, action: { self.actionsSubject.send(.delete) }))
case .cancel:
if state.bindings.hasContent {
if state.formContentHasChanged {
state.bindings.alertInfo = .init(id: .init(),
title: L10n.screenCreatePollDiscardConfirmationTitle,
message: L10n.screenCreatePollDiscardConfirmation,
@@ -17,7 +17,7 @@
import Combine
@MainActor
protocol CreatePollScreenViewModelProtocol {
var actions: AnyPublisher<CreatePollScreenViewModelAction, Never> { get }
var context: CreatePollScreenViewModelType.Context { get }
protocol PollFormScreenViewModelProtocol {
var actions: AnyPublisher<PollFormScreenViewModelAction, Never> { get }
var context: PollFormScreenViewModelType.Context { get }
}
@@ -17,35 +17,36 @@
import Compound
import SwiftUI
struct CreatePollScreen: View {
@ObservedObject var context: CreatePollScreenViewModel.Context
struct PollFormScreen: View {
@ObservedObject var context: PollFormScreenViewModel.Context
@FocusState var focus: Focus?
enum Focus: Hashable {
case question
case option(index: Int)
}
var body: some View {
Form {
questionSection
optionsSection
showResultsSection
deletePollSection
}
.track(screen: .createPoll)
.trackAnalyticsIfNeeded(context: context)
.compoundForm()
.scrollDismissesKeyboard(.immediately)
.environment(\.editMode, .constant(.active))
.navigationTitle(L10n.screenCreatePollTitle)
.navigationTitle(context.viewState.navigationTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
.animation(.elementDefault, value: context.options)
.interactiveDismissDisabled(context.viewState.bindings.hasContent)
.interactiveDismissDisabled(context.viewState.formContentHasChanged)
.alert(item: $context.alertInfo)
}
// MARK: - Private
private var questionSection: some View {
Section(L10n.screenCreatePollQuestionDesc) {
TextField(text: $context.question) {
@@ -57,7 +58,7 @@ struct CreatePollScreen: View {
}
.textFieldStyle(.compoundForm)
.focused($focus, equals: .question)
.accessibilityIdentifier(A11yIdentifiers.createPollScreen.question)
.accessibilityIdentifier(A11yIdentifiers.pollFormScreen.question)
.onSubmit {
focus = context.options.indices.first.map { .option(index: $0) }
}
@@ -65,22 +66,22 @@ struct CreatePollScreen: View {
}
.compoundFormSection()
}
private var optionsSection: some View {
Section {
ForEach(context.options) { option in
if let index = context.options.firstIndex(of: option) {
CreatePollOptionView(text: $context.options[index].text.limited(to: 240),
placeholder: L10n.screenCreatePollAnswerHint(index + 1),
canDeleteItem: context.options.count > 2) {
PollFormOptionView(text: $context.options[index].text.limited(to: 240),
placeholder: L10n.screenCreatePollAnswerHint(index + 1),
canDeleteItem: context.options.count > 2) {
if case .option(let focusedIndex) = focus, focusedIndex == index {
focus = nil
}
context.send(viewAction: .deleteOption(index: index))
}
.focused($focus, equals: .option(index: index))
.accessibilityIdentifier(A11yIdentifiers.createPollScreen.optionID(index))
.accessibilityIdentifier(A11yIdentifiers.pollFormScreen.optionID(index))
.onSubmit {
let nextOptionIndex = index == context.options.endIndex - 1 ? nil : index + 1
focus = nextOptionIndex.map { .option(index: $0) }
@@ -91,26 +92,43 @@ struct CreatePollScreen: View {
.onMove { offsets, toOffset in
context.options.move(fromOffsets: offsets, toOffset: toOffset)
}
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)
.accessibilityIdentifier(A11yIdentifiers.pollFormScreen.addOption)
}
}
.compoundFormSection()
}
private var showResultsSection: some View {
Section {
Toggle(L10n.screenCreatePollAnonymousDesc, isOn: $context.isUndisclosed)
.accessibilityIdentifier(A11yIdentifiers.createPollScreen.pollKind)
.accessibilityIdentifier(A11yIdentifiers.pollFormScreen.pollKind)
}
.compoundFormSection()
}
@ViewBuilder
private var deletePollSection: some View {
switch context.viewState.mode {
case .edit:
Section {
Button(role: .destructive) {
context.send(viewAction: .delete)
} label: {
Text(L10n.actionDeletePoll)
}
}
.compoundFormSection()
case .new:
EmptyView()
}
}
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
ToolbarItem(placement: .navigationBarLeading) {
@@ -118,24 +136,36 @@ struct CreatePollScreen: View {
context.send(viewAction: .cancel)
}
}
ToolbarItem(placement: .confirmationAction) {
Button(L10n.actionCreate) {
context.send(viewAction: .create)
Button(context.viewState.submitButtonTitle) {
context.send(viewAction: .submit)
}
.disabled(context.viewState.bindings.isCreateButtonDisabled)
.accessibilityIdentifier(A11yIdentifiers.createPollScreen.create)
.disabled(context.viewState.isSubmitButtonDisabled)
.accessibilityIdentifier(A11yIdentifiers.pollFormScreen.submit)
}
}
}
private struct CreatePollOptionView: View {
private extension View {
@MainActor @ViewBuilder
func trackAnalyticsIfNeeded(context: PollFormScreenViewModel.Context) -> some View {
switch context.viewState.mode {
case .edit:
self
case .new:
track(screen: .createPoll)
}
}
}
private struct PollFormOptionView: View {
@Environment(\.editMode) var editMode
@Binding var text: String
let placeholder: String
let canDeleteItem: Bool
let deleteAction: () -> Void
var body: some View {
HStack(spacing: 8) {
if editMode?.wrappedValue == .active {
@@ -160,11 +190,11 @@ private struct CreatePollOptionView: View {
// MARK: - Previews
struct CreatePollScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = CreatePollScreenViewModel()
struct PollFormScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = PollFormScreenViewModel(mode: .new)
static var previews: some View {
NavigationStack {
CreatePollScreen(context: viewModel.context)
PollFormScreen(context: viewModel.context)
}
}
}
@@ -36,7 +36,7 @@ enum RoomScreenCoordinatorAction {
case presentMediaUploadPreviewScreen(URL)
case presentRoomDetails
case presentLocationPicker
case presentPollForm
case presentPollForm(mode: PollFormMode)
case presentLocationViewer(body: String, geoURI: GeoURI, description: String?)
case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
case presentRoomMemberDetails(member: RoomMemberProxyProtocol)
@@ -104,8 +104,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.presentMediaUploadPicker(.documents))
case .displayLocationPicker:
actionsSubject.send(.presentLocationPicker)
case .displayPollForm:
actionsSubject.send(.presentPollForm)
case .displayPollForm(let mode):
actionsSubject.send(.presentPollForm(mode: mode))
case .displayMediaUploadPreviewScreen(let url):
actionsSubject.send(.presentMediaUploadPreviewScreen(url))
case .displayRoomMemberDetails(let member):
@@ -24,6 +24,7 @@ enum RoomScreenInteractionHandlerAction {
case displayReportContent(itemID: TimelineItemIdentifier, senderID: String)
case displayMessageForwarding(itemID: TimelineItemIdentifier)
case displayMediaUploadPreviewScreen(url: URL)
case displayPollForm(mode: PollFormMode)
case displayRoomMemberDetails(member: RoomMemberProxyProtocol)
case showActionMenu(TimelineItemActionMenuInfo)
case showDebugInfo(TimelineItemDebugInfo)
@@ -185,30 +186,18 @@ class RoomScreenInteractionHandler {
UIPasteboard.general.string = messageTimelineItem.body
case .edit:
guard let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol else {
return
}
let text: String
switch messageTimelineItem.contentType {
case .text(let textItem):
if ServiceLocator.shared.settings.richTextEditorEnabled, let formattedBodyHTMLString = textItem.formattedBodyHTMLString {
text = formattedBodyHTMLString
} else {
text = messageTimelineItem.body
}
case .emote(let emoteItem):
if ServiceLocator.shared.settings.richTextEditorEnabled, let formattedBodyHTMLString = emoteItem.formattedBodyHTMLString {
text = "/me " + formattedBodyHTMLString
} else {
text = "/me " + messageTimelineItem.body
switch timelineItem {
case let messageTimelineItem as EventBasedMessageTimelineItemProtocol:
processEditMessageEvent(messageTimelineItem)
case let pollTimelineItem as PollRoomTimelineItem:
guard let eventID = pollTimelineItem.id.eventID else {
MXLog.error("Cannot edit poll with id: \(timelineItem.id)")
return
}
actionsSubject.send(.displayPollForm(mode: .edit(eventID: eventID, poll: pollTimelineItem.poll)))
default:
text = messageTimelineItem.body
MXLog.error("Cannot edit item with id: \(timelineItem.id)")
}
actionsSubject.send(.composer(action: .setText(text: text)))
actionsSubject.send(.composer(action: .setMode(mode: .edit(originalItemId: messageTimelineItem.id))))
case .copyPermalink:
do {
guard let eventID = eventTimelineItem.id.eventID else {
@@ -258,6 +247,29 @@ class RoomScreenInteractionHandler {
}
}
private func processEditMessageEvent(_ messageTimelineItem: EventBasedMessageTimelineItemProtocol) {
let text: String
switch messageTimelineItem.contentType {
case .text(let textItem):
if ServiceLocator.shared.settings.richTextEditorEnabled, let formattedBodyHTMLString = textItem.formattedBodyHTMLString {
text = formattedBodyHTMLString
} else {
text = messageTimelineItem.body
}
case .emote(let emoteItem):
if ServiceLocator.shared.settings.richTextEditorEnabled, let formattedBodyHTMLString = emoteItem.formattedBodyHTMLString {
text = "/me " + formattedBodyHTMLString
} else {
text = "/me " + messageTimelineItem.body
}
default:
text = messageTimelineItem.body
}
actionsSubject.send(.composer(action: .setText(text: text)))
actionsSubject.send(.composer(action: .setMode(mode: .edit(originalItemId: messageTimelineItem.id))))
}
// MARK: Polls
func sendPollResponse(pollStartID: String, optionID: String) {
@@ -28,7 +28,7 @@ enum RoomScreenViewModelAction {
case displayMediaPicker
case displayDocumentPicker
case displayLocationPicker
case displayPollForm
case displayPollForm(mode: PollFormMode)
case displayMediaUploadPreviewScreen(url: URL)
case displayRoomMemberDetails(member: RoomMemberProxyProtocol)
case displayMessageForwarding(itemID: TimelineItemIdentifier)
@@ -66,6 +66,7 @@ enum RoomScreenComposerMode: Equatable {
enum RoomScreenViewPollAction {
case selectOption(pollStartID: String, optionID: String)
case end(pollStartID: String)
case edit(pollStartID: String, poll: Poll)
}
enum RoomScreenViewAudioAction {
@@ -188,8 +188,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
actionsSubject.send(.displayDocumentPicker)
case .displayLocationPicker:
actionsSubject.send(.displayLocationPicker)
case .displayPollForm:
actionsSubject.send(.displayPollForm)
case .displayNewPollForm:
actionsSubject.send(.displayPollForm(mode: .new))
case .handlePasteOrDrop(let provider):
roomScreenInteractionHandler.handlePasteOrDrop(provider)
case .composerModeChanged(mode: let mode):
@@ -213,6 +213,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
message: L10n.commonPollEndConfirmation,
primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil),
secondaryButton: .init(title: L10n.actionOk, action: { self.roomScreenInteractionHandler.endPoll(pollStartID: pollStartID) }))
case .edit(let pollStartID, let poll):
actionsSubject.send(.displayPollForm(mode: .edit(eventID: pollStartID, poll: poll)))
}
}
@@ -319,6 +321,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
actionsSubject.send(.displayEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis))
case .displayMessageForwarding(let itemID):
actionsSubject.send(.displayMessageForwarding(itemID: itemID))
case .displayPollForm(let mode):
actionsSubject.send(.displayPollForm(mode: mode))
case .displayReportContent(let itemID, let senderID):
actionsSubject.send(.displayReportContent(itemID: itemID, senderID: senderID))
case .displayMediaUploadPreviewScreen(let url):
@@ -202,12 +202,8 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
@ViewBuilder
var localizedSendInfo: some View {
HStack(spacing: 4) {
if let timelineItem = timelineItem as? TextBasedRoomTimelineItem {
Text(timelineItem.localizedSendInfo)
} else {
Text(timelineItem.timestamp)
}
Text(timelineItem.localizedSendInfo)
if adjustedDeliveryStatus == .sendingFailed {
CompoundIcon(\.error, size: .xSmall, relativeTo: .compound.bodyXS)
.accessibilityLabel(L10n.commonSendingFailed)
@@ -89,11 +89,11 @@ struct PollRoomTimelineView: View {
@ViewBuilder
private var toolbarView: some View {
if !poll.hasEnded, poll.createdByAccountOwner, let eventID {
if !poll.hasEnded, poll.createdByAccountOwner {
Button {
context.send(viewAction: .poll(.end(pollStartID: eventID)))
toolbarAction()
} label: {
Text(L10n.actionEndPoll)
Text(timelineItem.isEditable ? L10n.actionEditPoll : L10n.actionEndPoll)
.lineLimit(2, reservesSpace: false)
.font(.compound.bodyLGSemibold)
.foregroundColor(.compound.textOnSolidPrimary)
@@ -108,6 +108,18 @@ struct PollRoomTimelineView: View {
.padding(.top, 8)
}
}
private func toolbarAction() {
guard let eventID else {
return
}
if timelineItem.isEditable {
context.send(viewAction: .poll(.edit(pollStartID: eventID, poll: poll)))
} else {
context.send(viewAction: .poll(.end(pollStartID: eventID)))
}
}
private func progressBarColor(for option: Poll.Option) -> Color {
if poll.hasEnded {
@@ -194,5 +206,15 @@ struct PollRoomTimelineView_Previews: PreviewProvider, TestablePreview {
.environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context)
.previewDisplayName("Creator, disclosed, Plain")
PollRoomTimelineView(timelineItem: .mock(poll: .emptyDisclosed, isEditable: true))
.environment(\.timelineStyle, .bubbles)
.environmentObject(viewModel.context)
.previewDisplayName("Creator, no votes, Bubble")
PollRoomTimelineView(timelineItem: .mock(poll: .emptyDisclosed, isEditable: true))
.environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context)
.previewDisplayName("Creator, no votes, Plain")
}
}