From 634d8167eac6e4947d854d0f73600eb049edde7c Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Fri, 24 Nov 2023 16:47:58 +0000 Subject: [PATCH] Allow polls to be edited (#1869) Polls can be edited if they do not have any votes --------- Co-authored-by: ElementBot --- changelog.d/1869.feature | 1 + .../messages/impl/MessagesFlowNode.kt | 17 +- .../messages/impl/MessagesNavigator.kt | 1 + .../features/messages/impl/MessagesNode.kt | 5 + .../messages/impl/MessagesPresenter.kt | 40 ++- .../impl/actionlist/ActionListPresenter.kt | 5 +- .../messages/impl/timeline/TimelineEvents.kt | 4 + .../impl/timeline/TimelinePresenter.kt | 15 +- .../impl/timeline/TimelineStateProvider.kt | 2 + .../components/TimelineItemEventRow.kt | 1 + .../components/TimelineItemStateEventRow.kt | 1 + .../event/TimelineItemEventContentView.kt | 2 + .../components/event/TimelineItemPollView.kt | 10 +- .../event/TimelineItemEventFactory.kt | 1 + .../impl/timeline/model/TimelineItem.kt | 1 + .../TimelineItemLocationContentProvider.kt | 32 --- .../event/TimelineItemPollContentProvider.kt | 14 +- .../messages/impl/FakeMessagesNavigator.kt | 7 + .../messages/impl/MessagesPresenterTest.kt | 22 +- .../actionlist/ActionListPresenterTest.kt | 35 ++- .../messages/impl/fixtures/aMessageEvent.kt | 2 + .../impl/timeline/TimelinePresenterTest.kt | 22 +- .../groups/TimelineItemGrouperTest.kt | 1 + .../poll/api/PollAnswerViewProvider.kt | 13 +- .../features/poll/api/PollContentView.kt | 20 +- .../poll/api/create/CreatePollEntryPoint.kt | 11 +- .../poll/api/create/CreatePollMode.kt | 24 ++ .../poll/impl/create/CreatePollEvents.kt | 2 +- .../poll/impl/create/CreatePollException.kt | 27 ++ .../poll/impl/create/CreatePollNode.kt | 9 +- .../poll/impl/create/CreatePollPresenter.kt | 113 ++++++-- .../poll/impl/create/CreatePollState.kt | 10 +- .../impl/create/CreatePollStateProvider.kt | 24 +- .../poll/impl/create/CreatePollView.kt | 59 ++-- .../create/DefaultCreatePollEntryPoint.kt | 17 +- .../features/poll/impl/data/PollRepository.kt | 62 ++++ .../impl/src/main/res/values/localazy.xml | 1 + .../impl/create/CreatePollPresenterTest.kt | 272 +++++++++++++++--- .../libraries/matrix/api/room/MatrixRoom.kt | 17 ++ .../matrix/impl/room/RustMatrixRoom.kt | 24 ++ .../matrix/test/room/FakeMatrixRoom.kt | 27 +- .../matrix/test/room/RoomSummaryFixture.kt | 3 +- .../src/main/res/values/localazy.xml | 3 +- ...ditable-Day-10_11_null,NEXUS_5,1.0,en].png | 3 + ...table-Night-10_12_null,NEXUS_5,1.0,en].png | 3 + ...NoVotes-Day-10_11_null,NEXUS_5,1.0,en].png | 3 - ...Votes-Night-10_12_null,NEXUS_5,1.0,en].png | 3 - ...ollView-Day-0_0_null_6,NEXUS_5,1.0,en].png | 3 + ...lView-Night-0_1_null_6,NEXUS_5,1.0,en].png | 3 + tools/localazy/config.json | 3 +- 50 files changed, 827 insertions(+), 173 deletions(-) create mode 100644 changelog.d/1869.feature create mode 100644 features/poll/api/src/main/kotlin/io/element/android/features/poll/api/create/CreatePollMode.kt create mode 100644 features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollException.kt create mode 100644 features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_PollContentCreatorEditable_null_PollContentCreatorEditable-Day-10_11_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_PollContentCreatorEditable_null_PollContentCreatorEditable-Night-10_12_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_PollContentCreatorNoVotes_null_PollContentCreatorNoVotes-Day-10_11_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_PollContentCreatorNoVotes_null_PollContentCreatorNoVotes-Night-10_12_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Day-0_0_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Night-0_1_null_6,NEXUS_5,1.0,en].png diff --git a/changelog.d/1869.feature b/changelog.d/1869.feature new file mode 100644 index 0000000000..2c7da08935 --- /dev/null +++ b/changelog.d/1869.feature @@ -0,0 +1 @@ +Allow polls to be edited when they have not been voted on diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 128c531374..6176e5b598 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -50,6 +50,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.poll.api.create.CreatePollEntryPoint +import io.element.android.features.poll.api.create.CreatePollMode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode @@ -113,6 +114,9 @@ class MessagesFlowNode @AssistedInject constructor( @Parcelize data object CreatePoll : NavTarget + + @Parcelize + data class EditPoll(val eventId: EventId) : NavTarget } private val callback = plugins().firstOrNull() @@ -157,6 +161,10 @@ class MessagesFlowNode @AssistedInject constructor( backstack.push(NavTarget.CreatePoll) } + override fun onEditPollClicked(eventId: EventId) { + backstack.push(NavTarget.EditPoll(eventId)) + } + override fun onJoinCallClicked(roomId: RoomId) { val inputs = CallType.RoomCall( sessionId = matrixClient.sessionId, @@ -204,7 +212,14 @@ class MessagesFlowNode @AssistedInject constructor( sendLocationEntryPoint.createNode(this, buildContext) } NavTarget.CreatePoll -> { - createPollEntryPoint.createNode(this, buildContext) + createPollEntryPoint.nodeBuilder(this, buildContext) + .params(CreatePollEntryPoint.Params(mode = CreatePollMode.NewPoll)) + .build() + } + is NavTarget.EditPoll -> { + createPollEntryPoint.nodeBuilder(this, buildContext) + .params(CreatePollEntryPoint.Params(mode = CreatePollMode.EditPoll(eventId = navTarget.eventId))) + .build() } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt index a0517c59c4..cda8b7ddcd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt @@ -24,4 +24,5 @@ interface MessagesNavigator { fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) fun onForwardEventClicked(eventId: EventId) fun onReportContentClicked(eventId: EventId, senderId: UserId) + fun onEditPollClicked(eventId: EventId) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index 636569a24b..09883779e8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -66,6 +66,7 @@ class MessagesNode @AssistedInject constructor( fun onReportMessage(eventId: EventId, senderId: UserId) fun onSendLocationClicked() fun onCreatePollClicked() + fun onEditPollClicked(eventId: EventId) fun onJoinCallClicked(roomId: RoomId) } @@ -107,6 +108,10 @@ class MessagesNode @AssistedInject constructor( callback?.onReportMessage(eventId, senderId) } + override fun onEditPollClicked(eventId: EventId) { + callback?.onEditPollClicked(eventId) + } + private fun onSendLocationClicked() { callback?.onSendLocationClicked() } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index a00b673934..4496469c44 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -93,7 +93,7 @@ class MessagesPresenter @AssistedInject constructor( private val room: MatrixRoom, private val composerPresenter: MessageComposerPresenter, private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter, - private val timelinePresenter: TimelinePresenter, + timelinePresenterFactory: TimelinePresenter.Factory, private val actionListPresenter: ActionListPresenter, private val customReactionPresenter: CustomReactionPresenter, private val reactionSummaryPresenter: ReactionSummaryPresenter, @@ -110,6 +110,8 @@ class MessagesPresenter @AssistedInject constructor( private val buildMeta: BuildMeta, ) : Presenter { + private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator) + @AssistedFactory interface Factory { fun create(navigator: MessagesNavigator): MessagesPresenter @@ -294,20 +296,28 @@ class MessagesPresenter @AssistedInject constructor( composerState: MessageComposerState, enableTextFormatting: Boolean, ) { - val composerMode = MessageComposerMode.Edit( - targetEvent.eventId, - (targetEvent.content as? TimelineItemTextBasedContent)?.let { - if (enableTextFormatting) { - it.htmlBody ?: it.body - } else { - it.body - } - }.orEmpty(), - targetEvent.transactionId, - ) - composerState.eventSink( - MessageComposerEvents.SetMode(composerMode) - ) + when (targetEvent.content) { + is TimelineItemPollContent -> { + if (targetEvent.eventId == null) return + navigator.onEditPollClicked(targetEvent.eventId) + } + else -> { + val composerMode = MessageComposerMode.Edit( + targetEvent.eventId, + (targetEvent.content as? TimelineItemTextBasedContent)?.let { + if (enableTextFormatting) { + it.htmlBody ?: it.body + } else { + it.body + } + }.orEmpty(), + targetEvent.transactionId, + ) + composerState.eventSink( + MessageComposerEvents.SetMode(composerMode) + ) + } + } } private fun handleActionReply(targetEvent: TimelineItem.Event, composerState: MessageComposerState) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index e0b912e0ef..d936ea9af8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -112,7 +112,10 @@ class ActionListPresenter @Inject constructor( // Can only reply or forward messages already uploaded to the server add(TimelineItemAction.Reply) } - if (!timelineItem.content.isEnded && timelineItem.isRemote && isMineOrCanRedact) { + if (timelineItem.isRemote && timelineItem.isEditable) { + add(TimelineItemAction.Edit) + } + if (timelineItem.isRemote && !timelineItem.content.isEnded && isMineOrCanRedact) { add(TimelineItemAction.EndPoll) } if (timelineItem.content.canBeCopied()) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt index ca23904583..2fdcb947b4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt @@ -30,4 +30,8 @@ sealed interface TimelineEvents { data class PollEndClicked( val pollStartId: EventId, ) : TimelineEvents + + data class PollEditClicked( + val pollStartId: EventId, + ) : TimelineEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 0bad8f8830..6deb6e1839 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -27,8 +27,12 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.PollEnd import im.vector.app.features.analytics.plan.PollVote +import io.element.android.features.messages.impl.MessagesNavigator import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.session.SessionState @@ -54,16 +58,16 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject private const val BACK_PAGINATION_EVENT_LIMIT = 20 private const val BACK_PAGINATION_PAGE_SIZE = 50 -class TimelinePresenter @Inject constructor( +class TimelinePresenter @AssistedInject constructor( private val timelineItemsFactory: TimelineItemsFactory, private val room: MatrixRoom, private val dispatchers: CoroutineDispatchers, private val appScope: CoroutineScope, + @Assisted private val navigator: MessagesNavigator, private val analyticsService: AnalyticsService, private val verificationService: SessionVerificationService, private val encryptionService: EncryptionService, @@ -71,6 +75,11 @@ class TimelinePresenter @Inject constructor( private val redactedVoiceMessageManager: RedactedVoiceMessageManager, ) : Presenter { + @AssistedFactory + interface Factory { + fun create(navigator: MessagesNavigator): TimelinePresenter + } + private val timeline = room.timeline @Composable @@ -135,6 +144,8 @@ class TimelinePresenter @Inject constructor( ) analyticsService.capture(PollEnd()) } + is TimelineEvents.PollEditClicked -> + navigator.onEditPollClicked(event.pollStartId) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index 1cc7daa498..852c931186 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -118,6 +118,7 @@ internal fun aTimelineItemEvent( eventId: EventId = EventId("\$" + Random.nextInt().toString()), transactionId: TransactionId? = null, isMine: Boolean = false, + isEditable: Boolean = false, senderDisplayName: String = "Sender", content: TimelineItemEventContent = aTimelineItemTextContent(), groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None, @@ -139,6 +140,7 @@ internal fun aTimelineItemEvent( readReceiptState = readReceiptState, sentTime = "12:34", isMine = isMine, + isEditable = isEditable, senderDisplayName = senderDisplayName, groupPosition = groupPosition, localSendState = sendState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index 9642068704..2467d3a031 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -526,6 +526,7 @@ private fun MessageEventBubbleContent( TimelineItemEventContentView( content = event.content, isMine = event.isMine, + isEditable = event.isEditable, interactionSource = interactionSource, onClick = onMessageClick, onLongClick = onMessageLongClick, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt index ccffcc16ca..4d75994c5f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt @@ -68,6 +68,7 @@ fun TimelineItemStateEventRow( TimelineItemEventContentView( content = event.content, isMine = event.isMine, + isEditable = event.isEditable, interactionSource = interactionSource, onClick = onClick, onLongClick = onLongClick, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt index 22d1d9cacd..77537b9565 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt @@ -42,6 +42,7 @@ import io.element.android.libraries.architecture.Presenter fun TimelineItemEventContentView( content: TimelineItemEventContent, isMine: Boolean, + isEditable: Boolean, interactionSource: MutableInteractionSource, extraPadding: ExtraPadding, onClick: () -> Unit, @@ -103,6 +104,7 @@ fun TimelineItemEventContentView( is TimelineItemPollContent -> TimelineItemPollView( content = content, isMine = isMine, + isEditable = isEditable, eventSink = eventSink, modifier = modifier, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt index 958845f98c..c334b2cc56 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt @@ -32,6 +32,7 @@ import kotlinx.collections.immutable.toImmutableList fun TimelineItemPollView( content: TimelineItemPollContent, isMine: Boolean, + isEditable: Boolean, eventSink: (TimelineEvents) -> Unit, modifier: Modifier = Modifier, ) { @@ -43,15 +44,20 @@ fun TimelineItemPollView( eventSink(TimelineEvents.PollEndClicked(pollStartId)) } + fun onPollEdit(pollStartId: EventId) { + eventSink(TimelineEvents.PollEditClicked(pollStartId)) + } + PollContentView( eventId = content.eventId, question = content.question, answerItems = content.answerItems.toImmutableList(), pollKind = content.pollKind, isPollEnded = content.isEnded, + isPollEditable = isEditable, isMine = isMine, onAnswerSelected = ::onAnswerSelected, - onPollEdit = {}, // TODO Polls: Wire up this callback once poll edit screen is done. + onPollEdit = ::onPollEdit, onPollEnd = ::onPollEnd, modifier = modifier, ) @@ -64,6 +70,7 @@ internal fun TimelineItemPollViewPreview(@PreviewParameter(TimelineItemPollConte TimelineItemPollView( content = content, isMine = false, + isEditable = false, eventSink = {}, ) } @@ -75,6 +82,7 @@ internal fun TimelineItemPollCreatorViewPreview(@PreviewParameter(TimelineItemPo TimelineItemPollView( content = content, isMine = true, + isEditable = false, eventSink = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt index 837e84abc4..5e2d97e6a1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt @@ -88,6 +88,7 @@ class TimelineItemEventFactory @Inject constructor( senderAvatar = senderAvatarData, content = contentFactory.create(currentTimelineItem.event), isMine = currentTimelineItem.event.isOwn, + isEditable = currentTimelineItem.event.isEditable, sentTime = sentTime, groupPosition = groupPosition, reactionsState = currentTimelineItem.computeReactionsState(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt index db0d88b462..68dc8a0790 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt @@ -62,6 +62,7 @@ sealed interface TimelineItem { val content: TimelineItemEventContent, val sentTime: String = "", val isMine: Boolean = false, + val isEditable: Boolean, val groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None, val reactionsState: TimelineItemReactions, val readReceiptState: TimelineItemReadReceipts, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContentProvider.kt index 7ad79f19e8..dc5a03fa2b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContentProvider.kt @@ -18,10 +18,6 @@ package io.element.android.features.messages.impl.timeline.model.event import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.location.api.Location -import io.element.android.features.poll.api.PollAnswerItem -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.poll.PollAnswer -import io.element.android.libraries.matrix.api.poll.PollKind open class TimelineItemLocationContentProvider : PreviewParameterProvider { override val values: Sequence @@ -41,31 +37,3 @@ fun aTimelineItemLocationContent(description: String? = null) = TimelineItemLoca description = description, ) -fun aTimelineItemPollContent( - isEnded: Boolean = false, -) = TimelineItemPollContent( - eventId = EventId("\$anEventId"), - question = "Some question?", - answerItems = listOf( - PollAnswerItem( - answer = PollAnswer("id_1", "Answer1"), - isSelected = false, - isEnabled = false, - isWinner = false, - isDisclosed = false, - votesCount = 0, - percentage = 0.0f, - ), - PollAnswerItem( - answer = PollAnswer("id_2", "Answer2"), - isSelected = false, - isEnabled = false, - isWinner = false, - isDisclosed = false, - votesCount = 0, - percentage = 0.0f, - ), - ), - pollKind = PollKind.Disclosed, - isEnded = isEnded, -) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt index c475f6dfbd..d20f9fa856 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt @@ -17,7 +17,9 @@ package io.element.android.features.messages.impl.timeline.model.event import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.poll.api.PollAnswerItem import io.element.android.features.poll.api.aPollAnswerItemList +import io.element.android.features.poll.api.aPollQuestion import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.PollKind @@ -29,12 +31,16 @@ open class TimelineItemPollContentProvider : PreviewParameterProvider = aPollAnswerItemList(), + isEnded: Boolean = false, +): TimelineItemPollContent { return TimelineItemPollContent( eventId = EventId("\$anEventId"), pollKind = PollKind.Disclosed, - question = "What type of food should we have at the party?", - answerItems = aPollAnswerItemList(), - isEnded = false, + question = question, + answerItems = answerItems, + isEnded = isEnded, ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/FakeMessagesNavigator.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/FakeMessagesNavigator.kt index a1666b40e0..de2b2e5bf5 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/FakeMessagesNavigator.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/FakeMessagesNavigator.kt @@ -30,6 +30,9 @@ class FakeMessagesNavigator : MessagesNavigator { var onReportContentClickedCount = 0 private set + var onEditPollClickedCount = 0 + private set + override fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { onShowEventDebugInfoClickedCount++ } @@ -41,4 +44,8 @@ class FakeMessagesNavigator : MessagesNavigator { override fun onReportContentClicked(eventId: EventId, senderId: UserId) { onReportContentClickedCount++ } + + override fun onEditPollClicked(eventId: EventId) { + onEditPollClickedCount++ + } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 1f7aab637a..5410ac0695 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -327,6 +327,20 @@ class MessagesPresenterTest { } } + @Test + fun `present - handle action edit poll`() = runTest { + val navigator = FakeMessagesNavigator() + val presenter = createMessagesPresenter(navigator = navigator) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent(content = aTimelineItemPollContent()))) + assertThat(navigator.onEditPollClickedCount).isEqualTo(1) + } + } + @Test fun `present - handle action redact`() = runTest { val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) @@ -672,12 +686,18 @@ class MessagesPresenterTest { room = matrixRoom, dispatchers = coroutineDispatchers, appScope = this, + navigator = navigator, analyticsService = analyticsService, encryptionService = FakeEncryptionService(), verificationService = FakeSessionVerificationService(), featureFlagService = FakeFeatureFlagService(), redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), ) + val timelinePresenterFactory = object: TimelinePresenter.Factory { + override fun create(navigator: MessagesNavigator): TimelinePresenter { + return timelinePresenter + } + } val preferencesStore = InMemoryPreferencesStore(isRichTextEditorEnabled = true) val actionListPresenter = ActionListPresenter(preferencesStore = preferencesStore) val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter() @@ -688,7 +708,7 @@ class MessagesPresenterTest { room = matrixRoom, composerPresenter = messageComposerPresenter, voiceMessageComposerPresenter = voiceMessageComposerPresenter, - timelinePresenter = timelinePresenter, + timelinePresenterFactory = timelinePresenterFactory, actionListPresenter = actionListPresenter, customReactionPresenter = customReactionPresenter, reactionSummaryPresenter = reactionSummaryPresenter, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt index c462a1eae9..c2a8cd40d2 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt @@ -29,6 +29,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent +import io.element.android.features.poll.api.aPollAnswerItemList import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.tests.testutils.WarmUpRule @@ -407,7 +408,7 @@ class ActionListPresenterTest { } @Test - fun `present - compute for poll message`() = runTest { + fun `present - compute for editable poll message`() = runTest { val presenter = createActionListPresenter(isDeveloperModeEnabled = false) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -415,7 +416,36 @@ class ActionListPresenterTest { val initialState = awaitItem() val messageEvent = aMessageEvent( isMine = true, - content = aTimelineItemPollContent(), + isEditable = true, + content = aTimelineItemPollContent(answerItems = aPollAnswerItemList(hasVotes = false)), + ) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + messageEvent, + persistentListOf( + TimelineItemAction.Reply, + TimelineItemAction.Edit, + TimelineItemAction.EndPoll, + TimelineItemAction.Redact, + ) + ) + ) + assertThat(successState.displayEmojiReactions).isTrue() + } + } + @Test + fun `present - compute for non-editable poll message`() = runTest { + val presenter = createActionListPresenter(isDeveloperModeEnabled = false) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent( + isMine = true, + isEditable = false, + content = aTimelineItemPollContent(answerItems = aPollAnswerItemList(hasVotes = true)), ) initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) val successState = awaitItem() @@ -442,6 +472,7 @@ class ActionListPresenterTest { val initialState = awaitItem() val messageEvent = aMessageEvent( isMine = true, + isEditable = false, content = aTimelineItemPollContent(isEnded = true), ) initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true)) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/aMessageEvent.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/aMessageEvent.kt index 4e21c001b8..2c9f624e54 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/aMessageEvent.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/aMessageEvent.kt @@ -38,6 +38,7 @@ import kotlinx.collections.immutable.toImmutableList internal fun aMessageEvent( eventId: EventId? = AN_EVENT_ID, isMine: Boolean = true, + isEditable: Boolean = true, content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false), inReplyTo: InReplyToDetails? = null, isThreaded: Boolean = false, @@ -52,6 +53,7 @@ internal fun aMessageEvent( content = content, sentTime = "", isMine = isMine, + isEditable = isEditable, reactionsState = aTimelineItemReactions(count = 0), readReceiptState = TimelineItemReadReceipts(emptyList().toImmutableList()), localSendState = sendState, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index 83fb402a7f..ef2b566569 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -22,6 +22,7 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.PollEnd import im.vector.app.features.analytics.plan.PollVote +import io.element.android.features.messages.impl.FakeMessagesNavigator import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory @@ -223,10 +224,10 @@ class TimelinePresenterTest { assertThat(initialState.hasNewItems).isFalse() assertThat(initialState.timelineItems.size).isEqualTo(0) val now = Date().time - val minuteInMilis = 60 * 1000 + val minuteInMillis = 60 * 1000 // Use index as a convenient value for timestamp val (alice, bob, charlie) = aMatrixUserList().take(3).mapIndexed { i, user -> - ReactionSender(senderId = user.userId, timestamp = now + i * minuteInMilis) + ReactionSender(senderId = user.userId, timestamp = now + i * minuteInMillis) } val oneReaction = listOf( EventReaction( @@ -312,6 +313,20 @@ class TimelinePresenterTest { } } + @Test + fun `present - PollEditClicked event navigates`() = runTest { + val navigator = FakeMessagesNavigator() + val presenter = createTimelinePresenter( + messagesNavigator = navigator, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(TimelineEvents.PollEditClicked(AN_EVENT_ID)) + assertThat(navigator.onEditPollClickedCount).isEqualTo(1) + } + } + @Test fun `present - side effect on redacted items is invoked`() = runTest { val redactedVoiceMessageManager = FakeRedactedVoiceMessageManager() @@ -337,12 +352,14 @@ class TimelinePresenterTest { timeline: MatrixTimeline = FakeMatrixTimeline(), timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory(), redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), + messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(), ): TimelinePresenter { return TimelinePresenter( timelineItemsFactory = timelineItemsFactory, room = FakeMatrixRoom(matrixTimeline = timeline), dispatchers = testCoroutineDispatchers(), appScope = this, + navigator = messagesNavigator, analyticsService = FakeAnalyticsService(), encryptionService = FakeEncryptionService(), verificationService = FakeSessionVerificationService(), @@ -360,6 +377,7 @@ class TimelinePresenterTest { room = room, dispatchers = testCoroutineDispatchers(), appScope = this, + navigator = FakeMessagesNavigator(), analyticsService = analyticsService, encryptionService = FakeEncryptionService(), verificationService = FakeSessionVerificationService(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt index d3440cb88b..f8579aaf15 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt @@ -44,6 +44,7 @@ class TimelineItemGrouperTest { reactionsState = aTimelineItemReactions(count = 0), readReceiptState = TimelineItemReadReceipts(emptyList().toImmutableList()), localSendState = LocalEventSendState.Sent(AN_EVENT_ID), + isEditable = false, inReplyTo = null, isThreaded = false, debugInfo = aTimelineItemDebugInfo(), diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt index 42f0c4f527..a4a8f2e156 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt @@ -19,6 +19,8 @@ package io.element.android.features.poll.api import io.element.android.libraries.matrix.api.poll.PollAnswer import kotlinx.collections.immutable.persistentListOf +fun aPollQuestion() = "What type of food should we have at the party?" + fun aPollAnswerItemList( hasVotes: Boolean = true, isEnded: Boolean = false, @@ -30,7 +32,7 @@ fun aPollAnswerItemList( isEnabled = !isEnded, isWinner = isEnded, votesCount = if (hasVotes) 5 else 0, - percentage = 0.5f + percentage = if (hasVotes) 0.5f else 0f ), aPollAnswerItem( answer = PollAnswer("option_2", "Chinese \uD83C\uDDE8\uD83C\uDDF3"), @@ -47,9 +49,14 @@ fun aPollAnswerItemList( isWinner = false, isSelected = true, votesCount = if (hasVotes) 1 else 0, - percentage = 0.1f + percentage = if (hasVotes) 0.1f else 0f + ), + aPollAnswerItem( + isDisclosed = isDisclosed, + isEnabled = !isEnded, + votesCount = if (hasVotes) 4 else 0, + percentage = if (hasVotes) 0.4f else 0f, ), - aPollAnswerItem(isDisclosed = isDisclosed, isEnabled = !isEnded), ) fun aPollAnswerItem( diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt index aa72d588fa..f4e0157084 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt @@ -55,6 +55,7 @@ fun PollContentView( question: String, answerItems: ImmutableList, pollKind: PollKind, + isPollEditable: Boolean, isPollEnded: Boolean, isMine: Boolean, onAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, @@ -103,8 +104,8 @@ fun PollContentView( if (isMine) { CreatorView( - votesCount = 1, // TODO Polls: set to `votesCount` when edit poll screen is implemented. isPollEnded = isPollEnded, + isPollEditable = isPollEditable, onPollEdit = ::onPollEdit, onPollEnd = { showConfirmation = true }, modifier = Modifier.fillMaxWidth(), @@ -197,26 +198,25 @@ private fun ColumnScope.UndisclosedPollBottomNotice( @Composable private fun CreatorView( - @Suppress("SameParameterValue") votesCount: Int, // TODO Polls: remove @Suppress when edit poll screen is implemented. isPollEnded: Boolean, + isPollEditable: Boolean, onPollEdit: () -> Unit, onPollEnd: () -> Unit, modifier: Modifier = Modifier ) { - if (!isPollEnded) { - if (votesCount == 0) { + when { + isPollEditable -> Button( text = stringResource(id = CommonStrings.action_edit_poll), onClick = onPollEdit, modifier = modifier, ) - } else { + !isPollEnded -> Button( text = stringResource(id = CommonStrings.action_end_poll), onClick = onPollEnd, modifier = modifier, ) - } } } @@ -229,6 +229,7 @@ internal fun PollContentUndisclosedPreview() = ElementPreview { answerItems = aPollAnswerItemList(isDisclosed = false), pollKind = PollKind.Undisclosed, isPollEnded = false, + isPollEditable = false, isMine = false, onAnswerSelected = { _, _ -> }, onPollEdit = {}, @@ -245,6 +246,7 @@ internal fun PollContentDisclosedPreview() = ElementPreview { answerItems = aPollAnswerItemList(), pollKind = PollKind.Disclosed, isPollEnded = false, + isPollEditable = false, isMine = false, onAnswerSelected = { _, _ -> }, onPollEdit = {}, @@ -261,6 +263,7 @@ internal fun PollContentEndedPreview() = ElementPreview { answerItems = aPollAnswerItemList(isEnded = true), pollKind = PollKind.Disclosed, isPollEnded = true, + isPollEditable = false, isMine = false, onAnswerSelected = { _, _ -> }, onPollEdit = {}, @@ -270,13 +273,14 @@ internal fun PollContentEndedPreview() = ElementPreview { @PreviewsDayNight @Composable -internal fun PollContentCreatorNoVotesPreview() = ElementPreview { +internal fun PollContentCreatorEditablePreview() = ElementPreview { PollContentView( eventId = EventId("\$anEventId"), question = "What type of food should we have at the party?", answerItems = aPollAnswerItemList(hasVotes = false, isEnded = false), pollKind = PollKind.Disclosed, isPollEnded = false, + isPollEditable = true, isMine = true, onAnswerSelected = { _, _ -> }, onPollEdit = {}, @@ -293,6 +297,7 @@ internal fun PollContentCreatorPreview() = ElementPreview { answerItems = aPollAnswerItemList(isEnded = false), pollKind = PollKind.Disclosed, isPollEnded = false, + isPollEditable = false, isMine = true, onAnswerSelected = { _, _ -> }, onPollEdit = {}, @@ -309,6 +314,7 @@ internal fun PollContentCreatorEndedPreview() = ElementPreview { answerItems = aPollAnswerItemList(isEnded = true), pollKind = PollKind.Disclosed, isPollEnded = true, + isPollEditable = false, isMine = true, onAnswerSelected = { _, _ -> }, onPollEdit = {}, diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/create/CreatePollEntryPoint.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/create/CreatePollEntryPoint.kt index abbb041374..0676584eca 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/create/CreatePollEntryPoint.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/create/CreatePollEntryPoint.kt @@ -21,5 +21,14 @@ import com.bumble.appyx.core.node.Node import io.element.android.libraries.architecture.FeatureEntryPoint interface CreatePollEntryPoint : FeatureEntryPoint { - fun createNode(parentNode: Node, buildContext: BuildContext): Node + data class Params( + val mode: CreatePollMode, + ) + + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun params(params: Params): NodeBuilder + fun build(): Node + } } diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/create/CreatePollMode.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/create/CreatePollMode.kt new file mode 100644 index 0000000000..36c9cf57df --- /dev/null +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/create/CreatePollMode.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 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. + */ + +package io.element.android.features.poll.api.create + +import io.element.android.libraries.matrix.api.core.EventId + +sealed interface CreatePollMode { + data object NewPoll : CreatePollMode + data class EditPoll(val eventId: EventId) : CreatePollMode +} diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollEvents.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollEvents.kt index 1251e07696..b763edffa1 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollEvents.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollEvents.kt @@ -19,7 +19,7 @@ package io.element.android.features.poll.impl.create import io.element.android.libraries.matrix.api.poll.PollKind sealed interface CreatePollEvents { - data object Create : CreatePollEvents + data object Save : CreatePollEvents data class SetQuestion(val question: String) : CreatePollEvents data class SetAnswer(val index: Int, val text: String) : CreatePollEvents data object AddAnswer : CreatePollEvents diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollException.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollException.kt new file mode 100644 index 0000000000..20c10c4716 --- /dev/null +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollException.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 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. + */ + +package io.element.android.features.poll.impl.create + +internal sealed class CreatePollException : Exception() { + data class GetPollFailed( + override val message: String?, override val cause: Throwable? + ) : CreatePollException() + + data class SavePollFailed( + override val message: String?, override val cause: Throwable? + ) : CreatePollException() +} diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt index 506c39c177..a38ed3bb88 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt @@ -26,6 +26,9 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.poll.api.create.CreatePollMode +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope import io.element.android.services.analytics.api.AnalyticsService @@ -37,7 +40,11 @@ class CreatePollNode @AssistedInject constructor( analyticsService: AnalyticsService, ) : Node(buildContext, plugins = plugins) { - private val presenter = presenterFactory.create(backNavigator = ::navigateUp) + data class Inputs(val mode: CreatePollMode) : NodeInputs + + private val inputs: Inputs = inputs() + + private val presenter = presenterFactory.create(backNavigator = ::navigateUp, mode = inputs.mode) init { lifecycle.subscribe( diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt index 44cc54a100..58a32584e0 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt @@ -17,6 +17,7 @@ package io.element.android.features.poll.impl.create import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -32,9 +33,11 @@ import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.Composer import im.vector.app.features.analytics.plan.PollCreation import io.element.android.features.messages.api.MessageComposerContext +import io.element.android.features.poll.api.create.CreatePollMode +import io.element.android.features.poll.impl.data.PollRepository import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.poll.PollAnswer import io.element.android.libraries.matrix.api.poll.PollKind -import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -47,26 +50,39 @@ private const val MAX_ANSWER_LENGTH = 240 private const val MAX_SELECTIONS = 1 class CreatePollPresenter @AssistedInject constructor( - private val room: MatrixRoom, + private val repository: PollRepository, private val analyticsService: AnalyticsService, private val messageComposerContext: MessageComposerContext, @Assisted private val navigateUp: () -> Unit, + @Assisted private val mode: CreatePollMode, ) : Presenter { @AssistedFactory interface Factory { - fun create(backNavigator: () -> Unit): CreatePollPresenter + fun create(backNavigator: () -> Unit, mode: CreatePollMode): CreatePollPresenter } @Composable override fun present(): CreatePollState { - var question: String by rememberSaveable { mutableStateOf("") } - var answers: List by rememberSaveable() { mutableStateOf(listOf("", "")) } + var answers: List by rememberSaveable { mutableStateOf(listOf("", "")) } var pollKind: PollKind by rememberSaveable(saver = pollKindSaver) { mutableStateOf(PollKind.Disclosed) } var showConfirmation: Boolean by rememberSaveable { mutableStateOf(false) } - val canCreate: Boolean by remember { derivedStateOf { canCreate(question, answers) } } + LaunchedEffect(Unit) { + if (mode is CreatePollMode.EditPoll) { + repository.getPoll(mode.eventId).onSuccess { + question = it.question + answers = it.answers.map(PollAnswer::text) + pollKind = it.kind + }.onFailure { + analyticsService.trackGetPollFailed(it) + navigateUp() + } + } + } + + val canSave: Boolean by remember { derivedStateOf { canSave(question, answers) } } val canAddAnswer: Boolean by remember { derivedStateOf { canAddAnswer(answers) } } val immutableAnswers: ImmutableList by remember { derivedStateOf { answers.toAnswers() } } @@ -74,29 +90,25 @@ class CreatePollPresenter @AssistedInject constructor( fun handleEvents(event: CreatePollEvents) { when (event) { - is CreatePollEvents.Create -> scope.launch { - if (canCreate) { - room.createPoll( + is CreatePollEvents.Save -> scope.launch { + if (canSave) { + repository.savePoll( + existingPollId = when (mode) { + is CreatePollMode.EditPoll -> mode.eventId + is CreatePollMode.NewPoll -> null + }, question = question, answers = answers, - maxSelections = MAX_SELECTIONS, pollKind = pollKind, - ) - analyticsService.capture( - Composer( - inThread = messageComposerContext.composerMode.inThread, - isEditing = messageComposerContext.composerMode.isEditing, - isReply = messageComposerContext.composerMode.isReply, - messageType = Composer.MessageType.Poll, - ) - ) - analyticsService.capture( - PollCreation( - action = PollCreation.Action.Create, + maxSelections = MAX_SELECTIONS, + ).onSuccess { + analyticsService.capturePollSaved( isUndisclosed = pollKind == PollKind.Undisclosed, numberOfAnswers = answers.size, ) - ) + }.onFailure { + analyticsService.trackSavePollFailed(it, mode) + } navigateUp() } else { Timber.d("Cannot create poll") @@ -135,7 +147,11 @@ class CreatePollPresenter @AssistedInject constructor( } return CreatePollState( - canCreate = canCreate, + mode = when (mode) { + is CreatePollMode.NewPoll -> CreatePollState.Mode.New + is CreatePollMode.EditPoll -> CreatePollState.Mode.Edit + }, + canSave = canSave, canAddAnswer = canAddAnswer, question = question, answers = immutableAnswers, @@ -144,16 +160,61 @@ class CreatePollPresenter @AssistedInject constructor( eventSink = ::handleEvents, ) } + + private fun AnalyticsService.capturePollSaved( + isUndisclosed: Boolean, + numberOfAnswers: Int, + ) { + capture( + Composer( + inThread = messageComposerContext.composerMode.inThread, + isEditing = mode is CreatePollMode.EditPoll, + isReply = messageComposerContext.composerMode.isReply, + messageType = Composer.MessageType.Poll, + ) + ) + capture( + PollCreation( + action = when (mode) { + is CreatePollMode.EditPoll -> PollCreation.Action.Edit + is CreatePollMode.NewPoll -> PollCreation.Action.Create + }, + isUndisclosed = isUndisclosed, + numberOfAnswers = numberOfAnswers, + ) + ) + } } -private fun canCreate( +private fun AnalyticsService.trackGetPollFailed(cause: Throwable) { + val exception = CreatePollException.GetPollFailed( + message = "Tried to edit poll but couldn't get poll", + cause = cause, + ) + Timber.e(exception) + trackError(exception) +} + +private fun AnalyticsService.trackSavePollFailed(cause: Throwable, mode: CreatePollMode) { + val exception = CreatePollException.SavePollFailed( + message = when (mode) { + CreatePollMode.NewPoll -> "Failed to create poll" + is CreatePollMode.EditPoll -> "Failed to edit poll" + }, + cause = cause, + ) + Timber.e(exception) + trackError(exception) +} + +private fun canSave( question: String, answers: List ) = question.isNotBlank() && answers.size >= MIN_ANSWERS && answers.all { it.isNotBlank() } private fun canAddAnswer(answers: List) = answers.size < MAX_ANSWERS -private fun List.toAnswers(): ImmutableList { +fun List.toAnswers(): ImmutableList { return map { answer -> Answer( text = answer, diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollState.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollState.kt index 0a7a9ad618..23069e1d78 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollState.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollState.kt @@ -20,14 +20,20 @@ import io.element.android.libraries.matrix.api.poll.PollKind import kotlinx.collections.immutable.ImmutableList data class CreatePollState( - val canCreate: Boolean, + val mode: Mode, + val canSave: Boolean, val canAddAnswer: Boolean, val question: String, val answers: ImmutableList, val pollKind: PollKind, val showConfirmation: Boolean, val eventSink: (CreatePollEvents) -> Unit, -) +) { + enum class Mode { + New, + Edit, + } +} data class Answer( val text: String, diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt index c1393e0da0..0df5293b19 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt @@ -25,6 +25,7 @@ class CreatePollStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aCreatePollState( + mode = CreatePollState.Mode.New, canCreate = false, canAddAnswer = true, question = "", @@ -36,6 +37,7 @@ class CreatePollStateProvider : PreviewParameterProvider { showConfirmation = false, ), aCreatePollState( + mode = CreatePollState.Mode.New, canCreate = true, canAddAnswer = true, question = "What type of food should we have?", @@ -47,6 +49,7 @@ class CreatePollStateProvider : PreviewParameterProvider { pollKind = PollKind.Undisclosed, ), aCreatePollState( + mode = CreatePollState.Mode.New, canCreate = true, canAddAnswer = true, question = "What type of food should we have?", @@ -58,6 +61,7 @@ class CreatePollStateProvider : PreviewParameterProvider { pollKind = PollKind.Undisclosed, ), aCreatePollState( + mode = CreatePollState.Mode.New, canCreate = true, canAddAnswer = true, question = "What type of food should we have?", @@ -71,6 +75,7 @@ class CreatePollStateProvider : PreviewParameterProvider { pollKind = PollKind.Undisclosed, ), aCreatePollState( + mode = CreatePollState.Mode.New, canCreate = true, canAddAnswer = false, question = "Should there be more than 20 answers?", @@ -100,6 +105,7 @@ class CreatePollStateProvider : PreviewParameterProvider { pollKind = PollKind.Undisclosed, ), aCreatePollState( + mode = CreatePollState.Mode.New, canCreate = true, canAddAnswer = true, question = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + @@ -120,11 +126,24 @@ class CreatePollStateProvider : PreviewParameterProvider { ), showConfirmation = false, pollKind = PollKind.Undisclosed, - ) + ), + aCreatePollState( + mode = CreatePollState.Mode.Edit, + canCreate = false, + canAddAnswer = true, + question = "", + answers = persistentListOf( + Answer("", false), + Answer("", false) + ), + pollKind = PollKind.Disclosed, + showConfirmation = false, + ), ) } private fun aCreatePollState( + mode: CreatePollState.Mode, canCreate: Boolean, canAddAnswer: Boolean, question: String, @@ -133,7 +152,8 @@ private fun aCreatePollState( pollKind: PollKind ): CreatePollState { return CreatePollState( - canCreate = canCreate, + mode = mode, + canSave = canCreate, canAddAnswer = canAddAnswer, question = question, answers = answers, diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt index b79054ba84..cb6b80eccb 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt @@ -47,8 +47,8 @@ import io.element.android.features.poll.impl.R import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.list.ListItemContent -import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.Icon @@ -67,7 +67,6 @@ import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -@OptIn(ExperimentalMaterial3Api::class) @Composable fun CreatePollView( state: CreatePollState, @@ -90,23 +89,11 @@ fun CreatePollView( Scaffold( modifier = modifier, topBar = { - TopAppBar( - title = { - Text( - text = stringResource(id = R.string.screen_create_poll_title), - style = ElementTheme.typography.aliasScreenTitle, - ) - }, - navigationIcon = { - BackButton(onClick = navBack) - }, - actions = { - TextButton( - text = stringResource(id = CommonStrings.action_create), - onClick = { state.eventSink(CreatePollEvents.Create) }, - enabled = state.canCreate, - ) - } + CreatePollTopAppBar( + mode = state.mode, + saveEnabled = state.canSave, + onBackPress = navBack, + onSaveClicked = { state.eventSink(CreatePollEvents.Save) } ) }, ) { paddingValues -> @@ -210,6 +197,40 @@ fun CreatePollView( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun CreatePollTopAppBar( + mode: CreatePollState.Mode, + saveEnabled: Boolean, + onBackPress: () -> Unit = {}, + onSaveClicked: () -> Unit = {}, +) { + TopAppBar( + title = { + Text( + text = when (mode) { + CreatePollState.Mode.New -> stringResource(id = R.string.screen_create_poll_title) + CreatePollState.Mode.Edit -> stringResource(id = R.string.screen_edit_poll_title) + }, + style = ElementTheme.typography.aliasScreenTitle, + ) + }, + navigationIcon = { + BackButton(onClick = onBackPress) + }, + actions = { + TextButton( + text = when (mode) { + CreatePollState.Mode.New -> stringResource(id = CommonStrings.action_create) + CreatePollState.Mode.Edit -> stringResource(id = CommonStrings.action_done) + }, + onClick = onSaveClicked, + enabled = saveEnabled, + ) + } + ) +} + @PreviewsDayNight @Composable internal fun CreatePollViewPreview( diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPoint.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPoint.kt index 1ce64deb88..713fe8effc 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPoint.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPoint.kt @@ -18,6 +18,7 @@ package io.element.android.features.poll.impl.create import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.poll.api.create.CreatePollEntryPoint import io.element.android.libraries.architecture.createNode @@ -26,7 +27,19 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultCreatePollEntryPoint @Inject constructor() : CreatePollEntryPoint { - override fun createNode(parentNode: Node, buildContext: BuildContext): Node { - return parentNode.createNode(buildContext) + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): CreatePollEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : CreatePollEntryPoint.NodeBuilder { + + override fun params(params: CreatePollEntryPoint.Params): CreatePollEntryPoint.NodeBuilder { + plugins += CreatePollNode.Inputs(mode = params.mode) + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } } } diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt new file mode 100644 index 0000000000..d40c5a1df4 --- /dev/null +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 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. + */ + +package io.element.android.features.poll.impl.data + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.poll.PollKind +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.event.PollContent +import kotlinx.coroutines.flow.first +import javax.inject.Inject + +class PollRepository @Inject constructor( + private val room: MatrixRoom, +) { + suspend fun getPoll(eventId: EventId): Result = runCatching { + room.timeline + .timelineItems + .first() + .asSequence() + .filterIsInstance() + .first { it.eventId == eventId } + .event + .content as PollContent + } + + suspend fun savePoll( + existingPollId: EventId?, + question: String, + answers: List, + pollKind: PollKind, + maxSelections: Int, + ): Result = when (existingPollId) { + null -> room.createPoll( + question = question, + answers = answers, + maxSelections = maxSelections, + pollKind = pollKind, + ) + else -> room.editPoll( + pollStartId = existingPollId, + question = question, + answers = answers, + maxSelections = maxSelections, + pollKind = pollKind, + ) + } +} diff --git a/features/poll/impl/src/main/res/values/localazy.xml b/features/poll/impl/src/main/res/values/localazy.xml index 3c5006d0af..3d0c16c4a1 100644 --- a/features/poll/impl/src/main/res/values/localazy.xml +++ b/features/poll/impl/src/main/res/values/localazy.xml @@ -9,4 +9,5 @@ "Question or topic" "What is the poll about?" "Create Poll" + "Edit poll" diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt index 4ea3ff1eca..69046ab808 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt @@ -18,14 +18,25 @@ package io.element.android.features.poll.impl.create import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow +import app.cash.turbine.TurbineTestContext import app.cash.turbine.test import com.google.common.truth.Truth import im.vector.app.features.analytics.plan.Composer import im.vector.app.features.analytics.plan.PollCreation import io.element.android.features.messages.test.FakeMessageComposerContext +import io.element.android.features.poll.api.create.CreatePollMode +import io.element.android.features.poll.impl.data.PollRepository +import io.element.android.libraries.matrix.api.poll.PollAnswer import io.element.android.libraries.matrix.api.poll.PollKind -import io.element.android.libraries.matrix.test.room.CreatePollInvocation +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.event.PollContent +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.room.SavePollInvocation import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.aPollContent +import io.element.android.libraries.matrix.test.room.anEventTimelineItem +import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.delay @@ -38,58 +49,73 @@ class CreatePollPresenterTest { @get:Rule val warmUpRule = WarmUpRule() + private val pollEventId = AN_EVENT_ID private var navUpInvocationsCount = 0 - private val fakeMatrixRoom = FakeMatrixRoom() + private val existingPoll = anExistingPoll() + private val fakeMatrixRoom = createFakeMatrixRoom(existingPoll) private val fakeAnalyticsService = FakeAnalyticsService() private val fakeMessageComposerContext = FakeMessageComposerContext() - private val presenter = CreatePollPresenter( - room = fakeMatrixRoom, - analyticsService = fakeAnalyticsService, - messageComposerContext = fakeMessageComposerContext, - navigateUp = { navUpInvocationsCount++ }, - ) - @Test fun `default state has proper default values`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - awaitItem().let { - Truth.assertThat(it.canCreate).isEqualTo(false) - Truth.assertThat(it.canAddAnswer).isEqualTo(true) - Truth.assertThat(it.question).isEqualTo("") - Truth.assertThat(it.answers).isEqualTo(listOf(Answer("", false), Answer("", false))) - Truth.assertThat(it.pollKind).isEqualTo(PollKind.Disclosed) - Truth.assertThat(it.showConfirmation).isEqualTo(false) - } + awaitDefaultItem() + } + } + + @Test + fun `in edit mode, poll values are loaded`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(AN_EVENT_ID)) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitDefaultItem() + awaitPollLoaded() + } + } + + @Test + fun `in edit mode, if poll doesn't exist, error is tracked and screen is closed`() = runTest { + val room = createFakeMatrixRoom(existingPoll = null) + val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(AN_EVENT_ID), room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitDefaultItem() + Truth.assertThat(fakeAnalyticsService.trackedErrors.filterIsInstance()).isNotEmpty() + Truth.assertThat(navUpInvocationsCount).isEqualTo(1) } } @Test fun `non blank question and 2 answers are required to create a poll`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initial = awaitItem() - Truth.assertThat(initial.canCreate).isEqualTo(false) + Truth.assertThat(initial.canSave).isFalse() initial.eventSink(CreatePollEvents.SetQuestion("A question?")) val questionSet = awaitItem() - Truth.assertThat(questionSet.canCreate).isEqualTo(false) + Truth.assertThat(questionSet.canSave).isFalse() questionSet.eventSink(CreatePollEvents.SetAnswer(0, "Answer 1")) val answer1Set = awaitItem() - Truth.assertThat(answer1Set.canCreate).isEqualTo(false) + Truth.assertThat(answer1Set.canSave).isFalse() answer1Set.eventSink(CreatePollEvents.SetAnswer(1, "Answer 2")) val answer2Set = awaitItem() - Truth.assertThat(answer2Set.canCreate).isEqualTo(true) + Truth.assertThat(answer2Set.canSave).isTrue() } } @Test - fun `create polls sends a poll start event`() = runTest { + fun `create poll sends a poll start event`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -98,11 +124,11 @@ class CreatePollPresenterTest { initial.eventSink(CreatePollEvents.SetAnswer(0, "Answer 1")) initial.eventSink(CreatePollEvents.SetAnswer(1, "Answer 2")) skipItems(3) - initial.eventSink(CreatePollEvents.Create) + initial.eventSink(CreatePollEvents.Save) delay(1) // Wait for the coroutine to finish Truth.assertThat(fakeMatrixRoom.createPollInvocations.size).isEqualTo(1) Truth.assertThat(fakeMatrixRoom.createPollInvocations.last()).isEqualTo( - CreatePollInvocation( + SavePollInvocation( question = "A question?", answers = listOf("Answer 1", "Answer 2"), maxSelections = 1, @@ -128,8 +154,104 @@ class CreatePollPresenterTest { } } + @Test + fun `when poll creation fails, error is tracked`() = runTest { + val error = Exception("cause") + fakeMatrixRoom.givenCreatePollResult(Result.failure(error)) + val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitDefaultItem().eventSink(CreatePollEvents.SetQuestion("A question?")) + awaitItem().eventSink(CreatePollEvents.SetAnswer(0, "Answer 1")) + awaitItem().eventSink(CreatePollEvents.SetAnswer(1, "Answer 2")) + awaitItem().eventSink(CreatePollEvents.Save) + delay(1) // Wait for the coroutine to finish + Truth.assertThat(fakeMatrixRoom.createPollInvocations).hasSize(1) + Truth.assertThat(fakeAnalyticsService.capturedEvents).isEmpty() + Truth.assertThat(fakeAnalyticsService.trackedErrors).hasSize(1) + Truth.assertThat(fakeAnalyticsService.trackedErrors).containsExactly( + CreatePollException.SavePollFailed("Failed to create poll", error) + ) + } + } + + @Test + fun `edit poll sends a poll edit event`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId)) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitDefaultItem() + awaitPollLoaded().apply { + eventSink(CreatePollEvents.SetQuestion("Changed question")) + } + awaitItem().apply { + eventSink(CreatePollEvents.SetAnswer(0, "Changed answer 1")) + } + awaitItem().apply { + eventSink(CreatePollEvents.SetAnswer(1, "Changed answer 2")) + } + awaitPollLoaded( + newQuestion = "Changed question", + newAnswer1 = "Changed answer 1", + newAnswer2 = "Changed answer 2", + ).apply { + eventSink(CreatePollEvents.Save) + } + delay(1) // Wait for the coroutine to finish + Truth.assertThat(fakeMatrixRoom.editPollInvocations.size).isEqualTo(1) + Truth.assertThat(fakeMatrixRoom.editPollInvocations.last()).isEqualTo( + SavePollInvocation( + question = "Changed question", + answers = listOf("Changed answer 1", "Changed answer 2", "Maybe"), + maxSelections = 1, + pollKind = PollKind.Disclosed + ) + ) + Truth.assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(2) + Truth.assertThat(fakeAnalyticsService.capturedEvents[0]).isEqualTo( + Composer( + inThread = false, + isEditing = true, + isReply = false, + messageType = Composer.MessageType.Poll, + ) + ) + Truth.assertThat(fakeAnalyticsService.capturedEvents[1]).isEqualTo( + PollCreation( + action = PollCreation.Action.Edit, + isUndisclosed = false, + numberOfAnswers = 3, + ) + ) + } + } + + @Test + fun `when edit poll fails, error is tracked`() = runTest { + val error = Exception("cause") + fakeMatrixRoom.givenEditPollResult(Result.failure(error)) + val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId)) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitDefaultItem() + awaitPollLoaded().eventSink(CreatePollEvents.SetAnswer(0, "A")) + awaitPollLoaded(newAnswer1 = "A").eventSink(CreatePollEvents.Save) + delay(1) // Wait for the coroutine to finish + Truth.assertThat(fakeMatrixRoom.editPollInvocations).hasSize(1) + Truth.assertThat(fakeAnalyticsService.capturedEvents).isEmpty() + Truth.assertThat(fakeAnalyticsService.trackedErrors).hasSize(1) + Truth.assertThat(fakeAnalyticsService.trackedErrors).containsExactly( + CreatePollException.SavePollFailed("Failed to edit poll", error) + ) + } + } + @Test fun `add answer button adds an empty answer and removing it removes it`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -139,7 +261,7 @@ class CreatePollPresenterTest { initial.eventSink(CreatePollEvents.AddAnswer) val answerAdded = awaitItem() Truth.assertThat(answerAdded.answers.size).isEqualTo(3) - Truth.assertThat(answerAdded.answers[2].text).isEqualTo("") + Truth.assertThat(answerAdded.answers[2].text).isEmpty() initial.eventSink(CreatePollEvents.RemoveAnswer(2)) val answerRemoved = awaitItem() @@ -149,6 +271,7 @@ class CreatePollPresenterTest { @Test fun `set question sets the question`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -161,6 +284,7 @@ class CreatePollPresenterTest { @Test fun `set poll answer sets the given poll answer`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -173,6 +297,7 @@ class CreatePollPresenterTest { @Test fun `set poll kind sets the poll kind`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -185,34 +310,37 @@ class CreatePollPresenterTest { @Test fun `can add options when between 2 and 20 and then no more`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initial = awaitItem() - Truth.assertThat(initial.canAddAnswer).isEqualTo(true) + Truth.assertThat(initial.canAddAnswer).isTrue() repeat(17) { initial.eventSink(CreatePollEvents.AddAnswer) - Truth.assertThat(awaitItem().canAddAnswer).isEqualTo(true) + Truth.assertThat(awaitItem().canAddAnswer).isTrue() } initial.eventSink(CreatePollEvents.AddAnswer) - Truth.assertThat(awaitItem().canAddAnswer).isEqualTo(false) + Truth.assertThat(awaitItem().canAddAnswer).isFalse() } } @Test fun `can delete option if there are more than 2`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initial = awaitItem() - Truth.assertThat(initial.answers.all { it.canDelete }).isEqualTo(false) + Truth.assertThat(initial.answers.all { it.canDelete }).isFalse() initial.eventSink(CreatePollEvents.AddAnswer) - Truth.assertThat(awaitItem().answers.all { it.canDelete }).isEqualTo(true) + Truth.assertThat(awaitItem().answers.all { it.canDelete }).isTrue() } } @Test fun `option with more than 240 char is truncated`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -224,6 +352,7 @@ class CreatePollPresenterTest { @Test fun `navBack event calls navBack lambda`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -236,31 +365,104 @@ class CreatePollPresenterTest { @Test fun `confirm nav back with blank fields calls nav back lambda`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initial = awaitItem() Truth.assertThat(navUpInvocationsCount).isEqualTo(0) - Truth.assertThat(initial.showConfirmation).isEqualTo(false) + Truth.assertThat(initial.showConfirmation).isFalse() initial.eventSink(CreatePollEvents.ConfirmNavBack) Truth.assertThat(navUpInvocationsCount).isEqualTo(1) } } @Test - fun `confirm nav back with non blank fields shows confirmation dialog and sending hide hids it`() = runTest { + fun `confirm nav back with non blank fields shows confirmation dialog and sending hides it`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initial = awaitItem() initial.eventSink(CreatePollEvents.SetQuestion("Non blank")) Truth.assertThat(navUpInvocationsCount).isEqualTo(0) - Truth.assertThat(awaitItem().showConfirmation).isEqualTo(false) + Truth.assertThat(awaitItem().showConfirmation).isFalse() initial.eventSink(CreatePollEvents.ConfirmNavBack) Truth.assertThat(navUpInvocationsCount).isEqualTo(0) - Truth.assertThat(awaitItem().showConfirmation).isEqualTo(true) + Truth.assertThat(awaitItem().showConfirmation).isTrue() initial.eventSink(CreatePollEvents.HideConfirmation) - Truth.assertThat(awaitItem().showConfirmation).isEqualTo(false) + Truth.assertThat(awaitItem().showConfirmation).isFalse() } } + + private suspend fun TurbineTestContext.awaitDefaultItem() = + awaitItem().apply { + Truth.assertThat(canSave).isFalse() + Truth.assertThat(canAddAnswer).isTrue() + Truth.assertThat(question).isEmpty() + Truth.assertThat(answers).isEqualTo(listOf(Answer("", false), Answer("", false))) + Truth.assertThat(pollKind).isEqualTo(PollKind.Disclosed) + Truth.assertThat(showConfirmation).isFalse() + } + + private suspend fun TurbineTestContext.awaitPollLoaded( + newQuestion: String? = null, + newAnswer1: String? = null, + newAnswer2: String? = null, + ) = + awaitItem().apply { + Truth.assertThat(canSave).isTrue() + Truth.assertThat(canAddAnswer).isTrue() + Truth.assertThat(question).isEqualTo(newQuestion ?: existingPoll.question) + Truth.assertThat(answers).isEqualTo(existingPoll.expectedAnswersState().toMutableList().apply { + newAnswer1?.let { this[0] = Answer(it, true) } + newAnswer2?.let { this[1] = Answer(it, true) } + }) + Truth.assertThat(pollKind).isEqualTo(existingPoll.kind) + } + + private fun createCreatePollPresenter( + mode: CreatePollMode = CreatePollMode.NewPoll, + room: MatrixRoom = fakeMatrixRoom, + ): CreatePollPresenter = CreatePollPresenter( + repository = PollRepository(room), + analyticsService = fakeAnalyticsService, + messageComposerContext = fakeMessageComposerContext, + navigateUp = { navUpInvocationsCount++ }, + mode = mode, + ) + + private fun createFakeMatrixRoom( + existingPoll: PollContent? = anExistingPoll(), + ) = FakeMatrixRoom( + matrixTimeline = FakeMatrixTimeline( + initialTimelineItems = existingPoll?.let { + listOf( + MatrixTimelineItem.Event( + 0, + anEventTimelineItem( + eventId = pollEventId, + content = it, + ) + ) + ) + }.orEmpty() + ) + ) +} + +private fun anExistingPoll() = aPollContent( + question = "Do you like polls?", + answers = listOf( + PollAnswer("1", "Yes"), + PollAnswer("2", "No"), + PollAnswer("2", "Maybe"), + ), +) + +private fun PollContent.expectedAnswersState() = answers.map { answer -> + Answer( + text = answer.text, + canDelete = answers.size > 2, + ) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index e9ad35dafa..19daf7d50d 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -176,6 +176,23 @@ interface MatrixRoom : Closeable { pollKind: PollKind, ): Result + /** + * Edit a poll in the room. + * + * @param pollStartId The event ID of the poll start event. + * @param question The question to ask. + * @param answers The list of answers. + * @param maxSelections The maximum number of answers that can be selected. + * @param pollKind The kind of poll to create. + */ + suspend fun editPoll( + pollStartId: EventId, + question: String, + answers: List, + maxSelections: Int, + pollKind: PollKind, + ): Result + /** * Send a response to a poll. * diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 35a61c1a1d..40b0f08798 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -477,6 +477,30 @@ class RustMatrixRoom( } } + override suspend fun editPoll( + pollStartId: EventId, + question: String, + answers: List, + maxSelections: Int, + pollKind: PollKind, + ): Result = withContext(roomDispatcher) { + runCatching { + val pollStartEvent = + innerRoom.getEventTimelineItemByEventId( + eventId = pollStartId.value + ) + pollStartEvent.use { + innerRoom.editPoll( + question = question, + answers = answers, + maxSelections = maxSelections.toUByte(), + pollKind = pollKind.toInner(), + editItem = pollStartEvent, + ) + } + } + } + override suspend fun sendPollResponse( pollStartId: EventId, answers: List diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index a041263e2c..531c792713 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -103,6 +103,7 @@ class FakeMatrixRoom( private var reportContentResult = Result.success(Unit) private var sendLocationResult = Result.success(Unit) private var createPollResult = Result.success(Unit) + private var editPollResult = Result.success(Unit) private var sendPollResponseResult = Result.success(Unit) private var endPollResult = Result.success(Unit) private var progressCallbackValues = emptyList>() @@ -130,8 +131,11 @@ class FakeMatrixRoom( private val _sentLocations = mutableListOf() val sentLocations: List = _sentLocations - private val _createPollInvocations = mutableListOf() - val createPollInvocations: List = _createPollInvocations + private val _createPollInvocations = mutableListOf() + val createPollInvocations: List = _createPollInvocations + + private val _editPollInvocations = mutableListOf() + val editPollInvocations: List = _editPollInvocations private val _sendPollResponseInvocations = mutableListOf() val sendPollResponseInvocations: List = _sendPollResponseInvocations @@ -375,10 +379,21 @@ class FakeMatrixRoom( maxSelections: Int, pollKind: PollKind ): Result = simulateLongTask { - _createPollInvocations.add(CreatePollInvocation(question, answers, maxSelections, pollKind)) + _createPollInvocations.add(SavePollInvocation(question, answers, maxSelections, pollKind)) return createPollResult } + override suspend fun editPoll( + pollStartId: EventId, + question: String, + answers: List, + maxSelections: Int, + pollKind: PollKind + ): Result = simulateLongTask { + _editPollInvocations.add(SavePollInvocation(question, answers, maxSelections, pollKind)) + return editPollResult + } + override suspend fun sendPollResponse( pollStartId: EventId, answers: List @@ -511,6 +526,10 @@ class FakeMatrixRoom( createPollResult = result } + fun givenEditPollResult(result: Result) { + editPollResult = result + } + fun givenSendPollResponseResult(result: Result) { sendPollResponseResult = result } @@ -544,7 +563,7 @@ data class SendLocationInvocation( val assetType: AssetType?, ) -data class CreatePollInvocation( +data class SavePollInvocation( val question: String, val answers: List, val maxSelections: Int, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt index d12f168789..3c7b3eb594 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt @@ -181,11 +181,12 @@ fun aTimelineItemDebugInfo( fun aPollContent( question: String = "Do you like polls?", + answers: List = listOf(PollAnswer("1", "Yes"), PollAnswer("2", "No")), ) = PollContent( question = question, kind = PollKind.Disclosed, maxSelections = 1u, - answers = listOf(PollAnswer("1", "Yes"), PollAnswer("2", "No")), + answers = answers, votes = mapOf(), endTime = null ) diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 054a6281e6..fae900dd9c 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -39,9 +39,11 @@ "Create" "Create a room" "Decline" + "Delete Poll" "Disable" "Done" "Edit" + "Edit poll" "Enable" "End poll" "Enter PIN" @@ -93,7 +95,6 @@ "Try again" "View source" "Yes" - "Edit poll" "About" "Acceptable use policy" "Advanced settings" diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_PollContentCreatorEditable_null_PollContentCreatorEditable-Day-10_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_PollContentCreatorEditable_null_PollContentCreatorEditable-Day-10_11_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0d4d0da7f1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_PollContentCreatorEditable_null_PollContentCreatorEditable-Day-10_11_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df811687c29293019304217575a763a6ea4de56fe33ad0319683adf32856876f +size 52088 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_PollContentCreatorEditable_null_PollContentCreatorEditable-Night-10_12_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_PollContentCreatorEditable_null_PollContentCreatorEditable-Night-10_12_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..77b0e89854 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_PollContentCreatorEditable_null_PollContentCreatorEditable-Night-10_12_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47471630367f690a33ecd09445a3789ed4304eb7ebf771c58af44314efae4269 +size 48428 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_PollContentCreatorNoVotes_null_PollContentCreatorNoVotes-Day-10_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_PollContentCreatorNoVotes_null_PollContentCreatorNoVotes-Day-10_11_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 2d9643e989..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_PollContentCreatorNoVotes_null_PollContentCreatorNoVotes-Day-10_11_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5fbe23f6b00973c497f791c37f9d884e5d6baa6e6f5376f10633be0cb3043d49 -size 52067 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_PollContentCreatorNoVotes_null_PollContentCreatorNoVotes-Night-10_12_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_PollContentCreatorNoVotes_null_PollContentCreatorNoVotes-Night-10_12_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 48329ff374..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_PollContentCreatorNoVotes_null_PollContentCreatorNoVotes-Night-10_12_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2a38081492746fdc6a4ae9f5408005456ab0f2143d4f937139d637aa39eabe77 -size 48435 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Day-0_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Day-0_0_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ff7fd38843 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Day-0_0_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:211955586e4a74622b2dcea13a1909dafce1e39f1f98c3a8a794244eafa0c7e3 +size 33398 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Night-0_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Night-0_1_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fe838d9ce2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Night-0_1_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9256ccb081b430a7eec8508cf73ac7814a54d50ff4bf911b85e84fc01f6f56d2 +size 31621 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index e611db953c..06805d49cc 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -157,7 +157,8 @@ { "name": ":features:poll:impl", "includeRegex": [ - "screen_create_poll_.*" + "screen_create_poll_.*", + "screen_edit_poll_.*" ] }, {