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 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")
}
}