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 8a374471e3..5f67fd1f7c 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 @@ -202,6 +202,7 @@ class MessagesPresenter @AssistedInject constructor( TimelineItemAction.Developer -> handleShowDebugInfoAction(targetEvent) TimelineItemAction.Forward -> handleForwardAction(targetEvent) TimelineItemAction.ReportContent -> handleReportAction(targetEvent) + TimelineItemAction.EndPoll -> handleEndPollAction(targetEvent) } } @@ -310,6 +311,11 @@ class MessagesPresenter @AssistedInject constructor( navigator.onReportContentClicked(event.eventId, event.senderId) } + private suspend fun handleEndPollAction(event: TimelineItem.Event) { + event.eventId?.let { room.endPoll(it, "The poll with event id: $it has ended.") } + // TODO Polls: Send poll end analytic + } + private suspend fun handleCopyContents(event: TimelineItem.Event) { val content = when (event.content) { is TimelineItemTextBasedContent -> event.content.body 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 e654365bcd..f5e28818c9 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 @@ -99,6 +99,16 @@ class ActionListPresenter @Inject constructor( } is TimelineItemPollContent -> { buildList { + val isMineOrCanRedact = timelineItem.isMine || userCanRedact + + // TODO Poll: Reply to poll + // if (timelineItem.isRemote) { + // // Can only reply or forward messages already uploaded to the server + // add(TimelineItemAction.Reply) + // } + if (!timelineItem.content.isEnded && timelineItem.isRemote && isMineOrCanRedact) { + add(TimelineItemAction.EndPoll) + } if (timelineItem.content.canBeCopied()) { add(TimelineItemAction.Copy) } @@ -108,7 +118,7 @@ class ActionListPresenter @Inject constructor( if (!timelineItem.isMine) { add(TimelineItemAction.ReportContent) } - if (timelineItem.isMine || userCanRedact) { + if (isMineOrCanRedact) { add(TimelineItemAction.Redact) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt index 09213d64b3..755d4c36e1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt @@ -23,6 +23,7 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemLocationContent +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -83,6 +84,15 @@ open class ActionListStateProvider : PreviewParameterProvider { ), displayEmojiReactions = false, ), + anActionListState().copy( + target = ActionListState.Target.Success( + event = aTimelineItemEvent(content = aTimelineItemPollContent()).copy( + reactionsState = reactionsState + ), + actions = aTimelineItemPollActionList(), + ), + displayEmojiReactions = false, + ), ) } } @@ -104,3 +114,11 @@ fun aTimelineItemActionList(): ImmutableList { TimelineItemAction.Developer, ) } +fun aTimelineItemPollActionList(): ImmutableList { + return persistentListOf( + TimelineItemAction.Reply, + TimelineItemAction.EndPoll, + TimelineItemAction.Developer, + TimelineItemAction.Redact, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt index b6141218eb..8dc48f892e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt @@ -35,4 +35,5 @@ sealed class TimelineItemAction( data object Edit : TimelineItemAction(CommonStrings.action_edit, VectorIcons.Edit) data object Developer : TimelineItemAction(CommonStrings.action_view_source, VectorIcons.DeveloperMode) data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, VectorIcons.ReportContent, destructive = true) + data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, VectorIcons.EndPoll) } 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 a21f262071..7ad79f19e8 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,6 +18,10 @@ 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 @@ -36,3 +40,32 @@ 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/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 2c542e0054..f5d0185789 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -72,6 +72,7 @@ import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilTimeout import io.element.android.tests.testutils.testCoroutineDispatchers +import io.element.android.tests.testutils.waitForPredicate import io.mockk.mockk import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -559,6 +560,24 @@ class MessagesPresenterTest { } } + @Test + fun `present - handle poll end`() = runTest { + val room = FakeMatrixRoom() + val presenter = createMessagePresenter(matrixRoom = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent())) + waitForPredicate { room.endPollInvocations.size == 1 } + cancelAndIgnoreRemainingEvents() + assertThat(room.endPollInvocations.size).isEqualTo(1) + assertThat(room.endPollInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID) + assertThat(room.endPollInvocations.first().text).isEqualTo("The poll with event id: \$anEventId has ended.") + // TODO Polls: Test poll end analytic + } + } + private fun TestScope.createMessagePresenter( coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), matrixRoom: MatrixRoom = FakeMatrixRoom(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt index 111b3a370d..a4ff484ff3 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt @@ -26,15 +26,11 @@ import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.timeline.aTimelineItemEvent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent +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.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 import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.core.aBuildMeta import kotlinx.collections.immutable.persistentListOf @@ -384,34 +380,34 @@ class ActionListPresenterTest { val initialState = awaitItem() val messageEvent = aMessageEvent( isMine = true, - content = 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 = false, + content = aTimelineItemPollContent(), + ) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false)) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + messageEvent, + persistentListOf( + TimelineItemAction.EndPoll, + TimelineItemAction.Redact, + ) ) ) + assertThat(successState.displayEmojiReactions).isTrue() + } + } + @Test + fun `present - compute for ended poll message`() = runTest { + val presenter = anActionListPresenter(isBuildDebuggable = false) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent( + isMine = true, + content = aTimelineItemPollContent(isEnded = true), + ) initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false)) val successState = awaitItem() assertThat(successState.target).isEqualTo( 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 ceb9513545..8e3de07574 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 @@ -71,7 +71,7 @@ fun CreatePollView( val navBack = { state.eventSink(CreatePollEvents.ConfirmNavBack) } BackHandler(onBack = navBack) if (state.showConfirmation) ConfirmationDialog( - content = stringResource(id = R.string.screen_create_poll_confirmation), + content = stringResource(id = R.string.screen_create_poll_discard_confirmation), onSubmitClicked = { state.eventSink(CreatePollEvents.NavBack) }, onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) } ) diff --git a/features/poll/impl/src/main/res/values/localazy.xml b/features/poll/impl/src/main/res/values/localazy.xml index 846b247108..876dd0ee44 100644 --- a/features/poll/impl/src/main/res/values/localazy.xml +++ b/features/poll/impl/src/main/res/values/localazy.xml @@ -4,7 +4,7 @@ "Show results only after poll ends" "Anonymous Poll" "Option %1$d" - "Are you sure you would like to go back?" + "Are you sure you would like to go back?" "Question or topic" "What is the poll about?" "Create Poll" diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt index 47b951baa2..51755ef966 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt @@ -27,4 +27,5 @@ object VectorIcons { val ReportContent = R.drawable.ic_report_content val Groups = R.drawable.ic_groups val Share = R.drawable.ic_share + val EndPoll = R.drawable.ic_done_24 } diff --git a/libraries/designsystem/src/main/res/drawable/ic_done_24.xml b/libraries/designsystem/src/main/res/drawable/ic_done_24.xml new file mode 100644 index 0000000000..280f0bd804 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_done_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 4f25e8f07b..95ae43dd21 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -61,6 +61,7 @@ "Take photo" "View Source" "Yes" + "End poll" "About" "Acceptable use policy" "Analytics" diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-0_1_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-0_1_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6ac8e597c5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-0_1_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b0cc63408163b06093d0c8d238eb9451bf526af518c8f09c53a96b33d72c10f +size 23135 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-0_2_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-0_2_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..11192bb213 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-0_2_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7026daf71085ac6c62b66f93c1002afd1f5bb28446aa896fac8b11f2750ac80c +size 22317