Show poll creator view in timeline (#1429)

- Shows edit/end poll buttons when the user is the creator of the poll.
- Only the end poll button is wired right now as there is no "edit poll" screen yet.
This commit is contained in:
Marco Romano
2023-09-26 11:19:24 +02:00
committed by GitHub
parent 54fb1e79c5
commit 295e10e612
109 changed files with 234 additions and 17 deletions

View File

@@ -30,7 +30,6 @@ import androidx.compose.runtime.setValue
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.PollEnd
import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
@@ -39,6 +38,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.features.messages.impl.timeline.TimelineState
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
@@ -76,7 +76,6 @@ import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
import io.element.android.libraries.matrix.ui.room.canRedactAsState
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
import io.element.android.libraries.textcomposer.MessageComposerMode
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -95,7 +94,6 @@ class MessagesPresenter @AssistedInject constructor(
private val messageSummaryFormatter: MessageSummaryFormatter,
private val dispatchers: CoroutineDispatchers,
private val clipboardHelper: ClipboardHelper,
private val analyticsService: AnalyticsService,
private val preferencesStore: PreferencesStore,
@Assisted private val navigator: MessagesNavigator,
) : Presenter<MessagesState> {
@@ -155,6 +153,7 @@ class MessagesPresenter @AssistedInject constructor(
targetEvent = event.event,
composerState = composerState,
enableTextFormatting = enableTextFormatting,
timelineState = timelineState,
)
}
is MessagesEvents.ToggleReaction -> {
@@ -206,6 +205,7 @@ class MessagesPresenter @AssistedInject constructor(
targetEvent: TimelineItem.Event,
composerState: MessageComposerState,
enableTextFormatting: Boolean,
timelineState: TimelineState,
) = launch {
when (action) {
TimelineItemAction.Copy -> handleCopyContents(targetEvent)
@@ -216,7 +216,7 @@ class MessagesPresenter @AssistedInject constructor(
TimelineItemAction.ViewSource -> handleShowDebugInfoAction(targetEvent)
TimelineItemAction.Forward -> handleForwardAction(targetEvent)
TimelineItemAction.ReportContent -> handleReportAction(targetEvent)
TimelineItemAction.EndPoll -> handleEndPollAction(targetEvent)
TimelineItemAction.EndPoll -> handleEndPollAction(targetEvent, timelineState)
}
}
@@ -266,7 +266,7 @@ class MessagesPresenter @AssistedInject constructor(
targetEvent: TimelineItem.Event,
composerState: MessageComposerState,
enableTextFormatting: Boolean,
) {
) {
val composerMode = MessageComposerMode.Edit(
targetEvent.eventId,
(targetEvent.content as? TimelineItemTextBasedContent)?.let {
@@ -344,11 +344,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.")
analyticsService.capture(PollEnd())
}
private fun handleEndPollAction(
event: TimelineItem.Event,
timelineState: TimelineState,
) {
event.eventId?.let { timelineState.eventSink(TimelineEvents.PollEndClicked(it)) }
}
private suspend fun handleCopyContents(event: TimelineItem.Event) {

View File

@@ -108,7 +108,7 @@ class ActionListPresenter @Inject constructor(
buildList {
val isMineOrCanRedact = timelineItem.isMine || userCanRedact
// TODO Poll: Reply to poll. Ensure to update `fun TimelineItemEventContent.canBeReplied()`
// TODO Polls: Reply to poll. Ensure to update `fun TimelineItemEventContent.canBeReplied()`
// when touching this
// if (timelineItem.isRemote) {
// // Can only reply or forward messages already uploaded to the server

View File

@@ -26,4 +26,8 @@ sealed interface TimelineEvents {
val pollStartId: EventId,
val answerId: String
) : TimelineEvents
data class PollEndClicked(
val pollStartId: EventId,
) : TimelineEvents
}

View File

@@ -26,6 +26,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import im.vector.app.features.analytics.plan.PollEnd
import im.vector.app.features.analytics.plan.PollVote
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
import io.element.android.features.messages.impl.timeline.model.TimelineItem
@@ -98,11 +99,18 @@ class TimelinePresenter @Inject constructor(
)
analyticsService.capture(PollVote())
}
is TimelineEvents.PollEndClicked -> appScope.launch {
room.endPoll(
pollStartId = event.pollStartId,
text = "The poll with event id: ${event.pollStartId} has ended."
)
analyticsService.capture(PollEnd())
}
}
}
LaunchedEffect(timelineItems.size) {
computeHasNewItems(timelineItems, prevMostRecentItemId, hasNewItems)
computeHasNewItems(timelineItems, prevMostRecentItemId, hasNewItems)
}
LaunchedEffect(Unit) {

View File

@@ -386,6 +386,7 @@ private fun MessageEventBubbleContent(
) {
TimelineItemEventContentView(
content = event.content,
isMine = event.isMine,
interactionSource = interactionSource,
onClick = onMessageClick,
onLongClick = onMessageLongClick,

View File

@@ -67,6 +67,7 @@ fun TimelineItemStateEventRow(
) {
TimelineItemEventContentView(
content = event.content,
isMine = event.isMine,
interactionSource = interactionSource,
onClick = onClick,
onLongClick = onLongClick,

View File

@@ -36,6 +36,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
@Composable
fun TimelineItemEventContentView(
content: TimelineItemEventContent,
isMine: Boolean,
interactionSource: MutableInteractionSource,
extraPadding: ExtraPadding,
onClick: () -> Unit,
@@ -95,6 +96,7 @@ fun TimelineItemEventContentView(
)
is TimelineItemPollContent -> TimelineItemPollView(
content = content,
isMine = isMine,
eventSink = eventSink,
modifier = modifier,
)

View File

@@ -31,6 +31,7 @@ import kotlinx.collections.immutable.toImmutableList
@Composable
fun TimelineItemPollView(
content: TimelineItemPollContent,
isMine: Boolean,
eventSink: (TimelineEvents) -> Unit,
modifier: Modifier = Modifier,
) {
@@ -38,13 +39,20 @@ fun TimelineItemPollView(
eventSink(TimelineEvents.PollAnswerSelected(pollStartId, answerId))
}
fun onPollEnd(pollStartId: EventId) {
eventSink(TimelineEvents.PollEndClicked(pollStartId))
}
PollContentView(
eventId = content.eventId,
question = content.question,
answerItems = content.answerItems.toImmutableList(),
pollKind = content.pollKind,
isPollEnded = content.isEnded,
isMine = isMine,
onAnswerSelected = ::onAnswerSelected,
onPollEdit = {}, // TODO Polls: Wire up this callback once poll edit screen is done.
onPollEnd = ::onPollEnd,
modifier = modifier,
)
}
@@ -55,6 +63,18 @@ internal fun TimelineItemPollViewPreview(@PreviewParameter(TimelineItemPollConte
ElementPreview {
TimelineItemPollView(
content = content,
isMine = false,
eventSink = {},
)
}
@PreviewsDayNight
@Composable
internal fun TimelineItemPollCreatorViewPreview(@PreviewParameter(TimelineItemPollContentProvider::class) content: TimelineItemPollContent) =
ElementPreview {
TimelineItemPollView(
content = content,
isMine = true,
eventSink = {},
)
}

View File

@@ -644,7 +644,6 @@ class MessagesPresenterTest {
messageSummaryFormatter = FakeMessageSummaryFormatter(),
navigator = navigator,
clipboardHelper = clipboardHelper,
analyticsService = analyticsService,
preferencesStore = preferencesStore,
dispatchers = coroutineDispatchers,
)

View File

@@ -20,7 +20,9 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
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.fixtures.aMessageEvent
import io.element.android.features.messages.fixtures.aTimelineItemsFactory
import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelinePresenter
@@ -42,6 +44,7 @@ import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitWithLatch
import io.element.android.tests.testutils.testCoroutineDispatchers
import io.element.android.tests.testutils.waitForPredicate
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
@@ -280,6 +283,29 @@ class TimelinePresenterTest {
assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollVote())
}
@Test
fun `present - PollEndClicked event calls into rust room api and analytics`() = runTest {
val room = FakeMatrixRoom()
val analyticsService = FakeAnalyticsService()
val presenter = createTimelinePresenter(
room = room,
analyticsService = analyticsService,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(TimelineEvents.PollEndClicked(aMessageEvent().eventId!!))
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.")
assertThat(analyticsService.capturedEvents.size).isEqualTo(1)
assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollEnd())
}
}
private fun TestScope.createTimelinePresenter(
timeline: MatrixTimeline = FakeMatrixTimeline(),
timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory()

View File

@@ -19,13 +19,17 @@ package io.element.android.features.poll.api
import io.element.android.libraries.matrix.api.poll.PollAnswer
import kotlinx.collections.immutable.persistentListOf
fun aPollAnswerItemList(isEnded: Boolean = false, isDisclosed: Boolean = true) = persistentListOf(
fun aPollAnswerItemList(
hasVotes: Boolean = true,
isEnded: Boolean = false,
isDisclosed: Boolean = true,
) = persistentListOf(
aPollAnswerItem(
answer = PollAnswer("option_1", "Italian \uD83C\uDDEE\uD83C\uDDF9"),
isDisclosed = isDisclosed,
isEnabled = !isEnded,
isWinner = isEnded,
votesCount = 5,
votesCount = if (hasVotes) 5 else 0,
percentage = 0.5f
),
aPollAnswerItem(
@@ -42,7 +46,7 @@ fun aPollAnswerItemList(isEnded: Boolean = false, isDisclosed: Boolean = true) =
isEnabled = !isEnded,
isWinner = false,
isSelected = true,
votesCount = 1,
votesCount = if (hasVotes) 1 else 0,
percentage = 0.1f
),
aPollAnswerItem(isDisclosed = isDisclosed, isEnabled = !isEnded),

View File

@@ -26,14 +26,19 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables
@@ -51,13 +56,37 @@ fun PollContentView(
answerItems: ImmutableList<PollAnswerItem>,
pollKind: PollKind,
isPollEnded: Boolean,
isMine: Boolean,
onAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit,
onPollEdit: (pollStartId: EventId) -> Unit,
onPollEnd: (pollStartId: EventId) -> Unit,
modifier: Modifier = Modifier,
) {
val votesCount = remember(answerItems) { answerItems.sumOf { it.votesCount } }
fun onAnswerSelected(pollAnswer: PollAnswer) {
eventId?.let { onAnswerSelected(it, pollAnswer.id) }
}
fun onPollEdit() {
eventId?.let { onPollEdit(it) }
}
fun onPollEnd() {
eventId?.let { onPollEnd(it) }
}
var showConfirmation: Boolean by remember { mutableStateOf(false) }
if (showConfirmation) ConfirmationDialog(
content = stringResource(id = CommonStrings.common_poll_end_confirmation),
onSubmitClicked = {
onPollEnd()
showConfirmation = false
},
onDismiss = { showConfirmation = false },
)
Column(
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp),
@@ -67,11 +96,20 @@ fun PollContentView(
PollAnswers(answerItems = answerItems, onAnswerSelected = ::onAnswerSelected)
if (isPollEnded || pollKind == PollKind.Disclosed) {
val votesCount = remember(answerItems) { answerItems.sumOf { it.votesCount } }
DisclosedPollBottomNotice(votesCount = votesCount)
} else {
UndisclosedPollBottomNotice()
}
if (isMine) {
CreatorView(
votesCount = 1, // TODO Polls: set to `votesCount` when edit poll screen is implemented.
isPollEnded = isPollEnded,
onPollEdit = ::onPollEdit,
onPollEnd = { showConfirmation = true },
modifier = Modifier.fillMaxWidth(),
)
}
}
}
@@ -157,6 +195,31 @@ private fun ColumnScope.UndisclosedPollBottomNotice(
)
}
@Composable
private fun CreatorView(
@Suppress("SameParameterValue") votesCount: Int, // TODO Polls: remove @Suppress when edit poll screen is implemented.
isPollEnded: Boolean,
onPollEdit: () -> Unit,
onPollEnd: () -> Unit,
modifier: Modifier = Modifier
) {
if (!isPollEnded) {
if (votesCount == 0) {
Button(
text = stringResource(id = CommonStrings.action_edit_poll),
onClick = onPollEdit,
modifier = modifier,
)
} else {
Button(
text = stringResource(id = CommonStrings.action_end_poll),
onClick = onPollEnd,
modifier = modifier,
)
}
}
}
@PreviewsDayNight
@Composable
internal fun PollContentUndisclosedPreview() = ElementPreview {
@@ -166,7 +229,10 @@ internal fun PollContentUndisclosedPreview() = ElementPreview {
answerItems = aPollAnswerItemList(isDisclosed = false),
pollKind = PollKind.Undisclosed,
isPollEnded = false,
isMine = false,
onAnswerSelected = { _, _ -> },
onPollEdit = {},
onPollEnd = {},
)
}
@@ -179,7 +245,10 @@ internal fun PollContentDisclosedPreview() = ElementPreview {
answerItems = aPollAnswerItemList(),
pollKind = PollKind.Disclosed,
isPollEnded = false,
isMine = false,
onAnswerSelected = { _, _ -> },
onPollEdit = {},
onPollEnd = {},
)
}
@@ -192,6 +261,57 @@ internal fun PollContentEndedPreview() = ElementPreview {
answerItems = aPollAnswerItemList(isEnded = true),
pollKind = PollKind.Disclosed,
isPollEnded = true,
isMine = false,
onAnswerSelected = { _, _ -> },
onPollEdit = {},
onPollEnd = {},
)
}
@PreviewsDayNight
@Composable
internal fun PollContentCreatorNoVotesPreview() = 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,
isMine = true,
onAnswerSelected = { _, _ -> },
onPollEdit = {},
onPollEnd = {},
)
}
@PreviewsDayNight
@Composable
internal fun PollContentCreatorPreview() = ElementPreview {
PollContentView(
eventId = EventId("\$anEventId"),
question = "What type of food should we have at the party?",
answerItems = aPollAnswerItemList(isEnded = false),
pollKind = PollKind.Disclosed,
isPollEnded = false,
isMine = true,
onAnswerSelected = { _, _ -> },
onPollEdit = {},
onPollEnd = {},
)
}
@PreviewsDayNight
@Composable
internal fun PollContentCreatorEndedPreview() = ElementPreview {
PollContentView(
eventId = EventId("\$anEventId"),
question = "What type of food should we have at the party?",
answerItems = aPollAnswerItemList(isEnded = true),
pollKind = PollKind.Disclosed,
isPollEnded = true,
isMine = true,
onAnswerSelected = { _, _ -> },
onPollEdit = {},
onPollEnd = {},
)
}

View File

@@ -71,6 +71,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_edit_poll">"Edit poll"</string>
<string name="common_about">"About"</string>
<string name="common_acceptable_use_policy">"Acceptable use policy"</string>
<string name="common_advanced_settings">"Advanced settings"</string>
@@ -149,6 +150,7 @@
<string name="common_verification_complete">"Verification complete"</string>
<string name="common_video">"Video"</string>
<string name="common_waiting">"Waiting…"</string>
<string name="common_poll_end_confirmation">"Are you sure you want to end this poll?"</string>
<string name="common_poll_summary">"Poll: %1$s"</string>
<string name="dialog_title_confirmation">"Confirmation"</string>
<string name="dialog_title_warning">"Warning"</string>

Some files were not shown because too many files have changed in this diff Show More