Poll end (#1182)
- Adds an "End Poll" item in the action list long press menu. - Shows only on remote polls that have not ended yet and only if the user is the creator or has redact powers. Closes https://github.com/vector-im/element-meta/issues/2026
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ActionListState> {
|
||||
),
|
||||
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> {
|
||||
TimelineItemAction.Developer,
|
||||
)
|
||||
}
|
||||
fun aTimelineItemPollActionList(): ImmutableList<TimelineItemAction> {
|
||||
return persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.EndPoll,
|
||||
TimelineItemAction.Developer,
|
||||
TimelineItemAction.Redact,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<TimelineItemLocationContent> {
|
||||
override val values: Sequence<TimelineItemLocationContent>
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) }
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<string name="screen_create_poll_anonymous_desc">"Show results only after poll ends"</string>
|
||||
<string name="screen_create_poll_anonymous_headline">"Anonymous Poll"</string>
|
||||
<string name="screen_create_poll_answer_hint">"Option %1$d"</string>
|
||||
<string name="screen_create_poll_confirmation">"Are you sure you would like to go back?"</string>
|
||||
<string name="screen_create_poll_discard_confirmation">"Are you sure you would like to go back?"</string>
|
||||
<string name="screen_create_poll_question_desc">"Question or topic"</string>
|
||||
<string name="screen_create_poll_question_hint">"What is the poll about?"</string>
|
||||
<string name="screen_create_poll_title">"Create Poll"</string>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
10
libraries/designsystem/src/main/res/drawable/ic_done_24.xml
Normal file
10
libraries/designsystem/src/main/res/drawable/ic_done_24.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M382,720L154,492L211,435L382,606L749,239L806,296L382,720Z"/>
|
||||
</vector>
|
||||
@@ -61,6 +61,7 @@
|
||||
<string name="action_take_photo">"Take photo"</string>
|
||||
<string name="action_view_source">"View Source"</string>
|
||||
<string name="action_yes">"Yes"</string>
|
||||
<string name="action_end_poll">"End poll"</string>
|
||||
<string name="common_about">"About"</string>
|
||||
<string name="common_acceptable_use_policy">"Acceptable use policy"</string>
|
||||
<string name="common_analytics">"Analytics"</string>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user