Polls: share logic around PollContent
This commit is contained in:
parent
d0d73e04c1
commit
4a2cbb1ed4
31 changed files with 571 additions and 149 deletions
|
|
@ -30,14 +30,14 @@ 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.NewEventState
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
||||
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
||||
import io.element.android.features.poll.api.actions.EndPollAction
|
||||
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
|
|
@ -52,7 +52,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemE
|
|||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
|
@ -69,11 +68,12 @@ class TimelinePresenter @AssistedInject constructor(
|
|||
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,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val redactedVoiceMessageManager: RedactedVoiceMessageManager,
|
||||
private val sendPollResponseAction: SendPollResponseAction,
|
||||
private val endPollAction: EndPollAction,
|
||||
) : Presenter<TimelineState> {
|
||||
|
||||
@AssistedFactory
|
||||
|
|
@ -132,18 +132,15 @@ class TimelinePresenter @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
is TimelineEvents.PollAnswerSelected -> appScope.launch {
|
||||
room.sendPollResponse(
|
||||
sendPollResponseAction.execute(
|
||||
pollStartId = event.pollStartId,
|
||||
answers = listOf(event.answerId),
|
||||
answerId = event.answerId
|
||||
)
|
||||
analyticsService.capture(PollVote())
|
||||
}
|
||||
is TimelineEvents.PollEndClicked -> appScope.launch {
|
||||
room.endPoll(
|
||||
endPollAction.execute(
|
||||
pollStartId = event.pollStartId,
|
||||
text = "The poll with event id: ${event.pollStartId} has ended."
|
||||
)
|
||||
analyticsService.capture(PollEnd())
|
||||
}
|
||||
is TimelineEvents.PollEditClicked ->
|
||||
navigator.onEditPollClicked(event.pollStartId)
|
||||
|
|
|
|||
|
|
@ -517,12 +517,10 @@ private fun MessageEventBubbleContent(
|
|||
) {
|
||||
TimelineItemEventContentView(
|
||||
content = event.content,
|
||||
isMine = event.isMine,
|
||||
isEditable = event.isEditable,
|
||||
interactionSource = interactionSource,
|
||||
extraPadding = event.toExtraPadding(),
|
||||
onClick = onMessageClick,
|
||||
onLongClick = onMessageLongClick,
|
||||
extraPadding = event.toExtraPadding(),
|
||||
eventSink = eventSink,
|
||||
modifier = contentModifier,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -80,12 +80,10 @@ fun TimelineItemStateEventRow(
|
|||
) {
|
||||
TimelineItemEventContentView(
|
||||
content = event.content,
|
||||
isMine = event.isMine,
|
||||
isEditable = event.isEditable,
|
||||
interactionSource = interactionSource,
|
||||
extraPadding = noExtraPadding,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
extraPadding = noExtraPadding,
|
||||
eventSink = eventSink,
|
||||
modifier = Modifier.defaultTimelineContentPadding()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -41,8 +41,6 @@ import io.element.android.libraries.architecture.Presenter
|
|||
@Composable
|
||||
fun TimelineItemEventContentView(
|
||||
content: TimelineItemEventContent,
|
||||
isMine: Boolean,
|
||||
isEditable: Boolean,
|
||||
interactionSource: MutableInteractionSource,
|
||||
extraPadding: ExtraPadding,
|
||||
onClick: () -> Unit,
|
||||
|
|
@ -103,8 +101,6 @@ fun TimelineItemEventContentView(
|
|||
)
|
||||
is TimelineItemPollContent -> TimelineItemPollView(
|
||||
content = content,
|
||||
isMine = isMine,
|
||||
isEditable = isEditable,
|
||||
eventSink = eventSink,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContentProvider
|
||||
import io.element.android.features.poll.api.PollContentView
|
||||
import io.element.android.features.poll.api.pollcontent.PollContentView
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
|
@ -31,8 +31,6 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
@Composable
|
||||
fun TimelineItemPollView(
|
||||
content: TimelineItemPollContent,
|
||||
isMine: Boolean,
|
||||
isEditable: Boolean,
|
||||
eventSink: (TimelineEvents) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -54,8 +52,8 @@ fun TimelineItemPollView(
|
|||
answerItems = content.answerItems.toImmutableList(),
|
||||
pollKind = content.pollKind,
|
||||
isPollEnded = content.isEnded,
|
||||
isPollEditable = isEditable,
|
||||
isMine = isMine,
|
||||
isPollEditable = content.isEditable,
|
||||
isMine = content.isMine,
|
||||
onAnswerSelected = ::onAnswerSelected,
|
||||
onPollEdit = ::onPollEdit,
|
||||
onPollEnd = ::onPollEnd,
|
||||
|
|
@ -69,8 +67,6 @@ internal fun TimelineItemPollViewPreview(@PreviewParameter(TimelineItemPollConte
|
|||
ElementPreview {
|
||||
TimelineItemPollView(
|
||||
content = content,
|
||||
isMine = false,
|
||||
isEditable = false,
|
||||
eventSink = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -81,8 +77,6 @@ internal fun TimelineItemPollCreatorViewPreview(@PreviewParameter(TimelineItemPo
|
|||
ElementPreview {
|
||||
TimelineItemPollView(
|
||||
content = content,
|
||||
isMine = true,
|
||||
isEditable = false,
|
||||
eventSink = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class TimelineItemContentFactory @Inject constructor(
|
|||
is RoomMembershipContent -> roomMembershipFactory.create(eventTimelineItem)
|
||||
is StateContent -> stateFactory.create(eventTimelineItem)
|
||||
is StickerContent -> stickerFactory.create(itemContent)
|
||||
is PollContent -> pollFactory.create(itemContent, eventTimelineItem.eventId)
|
||||
is PollContent -> pollFactory.create(eventTimelineItem, itemContent)
|
||||
is UnableToDecryptContent -> utdFactory.create(itemContent)
|
||||
is UnknownContent -> TimelineItemUnknownContent
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,63 +19,32 @@ package io.element.android.features.messages.impl.timeline.factories.event
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.features.poll.api.PollAnswerItem
|
||||
import io.element.android.features.poll.api.pollcontent.PollContentStateFactory
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.poll.isDisclosed
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemContentPollFactory @Inject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val pollContentStateFactory: PollContentStateFactory,
|
||||
) {
|
||||
|
||||
suspend fun create(
|
||||
event: EventTimelineItem,
|
||||
content: PollContent,
|
||||
eventId: EventId?
|
||||
): TimelineItemEventContent {
|
||||
if (!featureFlagService.isFeatureEnabled(FeatureFlags.Polls)) return TimelineItemUnknownContent
|
||||
|
||||
// Todo Move this computation to the matrix rust sdk
|
||||
val totalVoteCount = content.votes.flatMap { it.value }.size
|
||||
val myVotes = content.votes.filter { matrixClient.sessionId in it.value }.keys
|
||||
val isEndedPoll = content.endTime != null
|
||||
val winnerIds = if (!isEndedPoll) {
|
||||
emptyList()
|
||||
} else {
|
||||
content.answers
|
||||
.map { answer -> answer.id }
|
||||
.groupBy { answerId -> content.votes[answerId]?.size ?: 0 } // Group by votes count
|
||||
.maxByOrNull { (votes, _) -> votes } // Keep max voted answers
|
||||
?.takeIf { (votes, _) -> votes > 0 } // Ignore if no option has been voted
|
||||
?.value
|
||||
.orEmpty()
|
||||
}
|
||||
val answerItems = content.answers.map { answer ->
|
||||
val answerVoteCount = content.votes[answer.id]?.size ?: 0
|
||||
val isSelected = answer.id in myVotes
|
||||
val isWinner = answer.id in winnerIds
|
||||
val percentage = if (totalVoteCount > 0) answerVoteCount.toFloat() / totalVoteCount.toFloat() else 0f
|
||||
PollAnswerItem(
|
||||
answer = answer,
|
||||
isSelected = isSelected,
|
||||
isEnabled = !isEndedPoll,
|
||||
isWinner = isWinner,
|
||||
isDisclosed = content.kind.isDisclosed || isEndedPoll,
|
||||
votesCount = answerVoteCount,
|
||||
percentage = percentage,
|
||||
)
|
||||
}
|
||||
|
||||
val pollContentState = pollContentStateFactory.create(event, content)
|
||||
return TimelineItemPollContent(
|
||||
eventId = eventId,
|
||||
question = content.question,
|
||||
answerItems = answerItems,
|
||||
pollKind = content.kind,
|
||||
isEnded = isEndedPoll,
|
||||
isMine = pollContentState.isMine,
|
||||
isEditable = pollContentState.isPollEditable,
|
||||
eventId = event.eventId,
|
||||
question = pollContentState.question,
|
||||
answerItems = pollContentState.answerItems,
|
||||
pollKind = pollContentState.pollKind,
|
||||
isEnded = pollContentState.isPollEnded,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,11 +16,13 @@
|
|||
|
||||
package io.element.android.features.messages.impl.timeline.model.event
|
||||
|
||||
import io.element.android.features.poll.api.PollAnswerItem
|
||||
import io.element.android.features.poll.api.pollcontent.PollAnswerItem
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
|
||||
data class TimelineItemPollContent(
|
||||
val isMine: Boolean,
|
||||
val isEditable: Boolean,
|
||||
val eventId: EventId?,
|
||||
val question: String,
|
||||
val answerItems: List<PollAnswerItem>,
|
||||
|
|
|
|||
|
|
@ -17,9 +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.features.poll.api.pollcontent.aPollAnswerItemList
|
||||
import io.element.android.features.poll.api.pollcontent.aPollQuestion
|
||||
import io.element.android.features.poll.api.pollcontent.PollAnswerItem
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
|
||||
|
|
@ -28,12 +28,16 @@ open class TimelineItemPollContentProvider : PreviewParameterProvider<TimelineIt
|
|||
get() = sequenceOf(
|
||||
aTimelineItemPollContent(),
|
||||
aTimelineItemPollContent().copy(pollKind = PollKind.Undisclosed),
|
||||
aTimelineItemPollContent().copy(isMine = true),
|
||||
aTimelineItemPollContent().copy(isEditable = true),
|
||||
)
|
||||
}
|
||||
|
||||
fun aTimelineItemPollContent(
|
||||
question: String = aPollQuestion(),
|
||||
answerItems: List<PollAnswerItem> = aPollAnswerItemList(),
|
||||
isMine: Boolean = false,
|
||||
isEditable: Boolean = false,
|
||||
isEnded: Boolean = false,
|
||||
): TimelineItemPollContent {
|
||||
return TimelineItemPollContent(
|
||||
|
|
@ -41,6 +45,8 @@ fun aTimelineItemPollContent(
|
|||
pollKind = PollKind.Disclosed,
|
||||
question = question,
|
||||
answerItems = answerItems,
|
||||
isMine = isMine,
|
||||
isEditable = isEditable,
|
||||
isEnded = isEnded,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +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.features.poll.api.pollcontent.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
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ package io.element.android.features.messages.impl.timeline.factories.event
|
|||
|
||||
import com.google.common.truth.Truth
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||
import io.element.android.features.poll.api.PollAnswerItem
|
||||
import io.element.android.features.poll.api.pollcontent.PollAnswerItem
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
|
@ -55,14 +55,14 @@ internal class TimelineItemContentPollFactoryTest {
|
|||
|
||||
@Test
|
||||
fun `Disclosed poll - not ended, no votes`() = runTest {
|
||||
Truth.assertThat(factory.create(aPollContent(), eventId = null)).isEqualTo(aTimelineItemPollContent())
|
||||
Truth.assertThat(factory.create(aPollContent(), eventId = null, eventTimelineItem.isOwn)).isEqualTo(aTimelineItemPollContent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Disclosed poll - not ended, some votes, including one from current user`() = runTest {
|
||||
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||
Truth.assertThat(
|
||||
factory.create(aPollContent(votes = votes), eventId = null)
|
||||
factory.create(aPollContent(votes = votes), eventId = null, eventTimelineItem.isOwn)
|
||||
)
|
||||
.isEqualTo(
|
||||
aTimelineItemPollContent(
|
||||
|
|
@ -79,7 +79,7 @@ internal class TimelineItemContentPollFactoryTest {
|
|||
@Test
|
||||
fun `Disclosed poll - ended, no votes, no winner`() = runTest {
|
||||
Truth.assertThat(
|
||||
factory.create(aPollContent(endTime = 1UL), eventId = null)
|
||||
factory.create(aPollContent(endTime = 1UL), eventId = null, eventTimelineItem.isOwn)
|
||||
).isEqualTo(
|
||||
aTimelineItemPollContent().let {
|
||||
it.copy(
|
||||
|
|
@ -94,7 +94,11 @@ internal class TimelineItemContentPollFactoryTest {
|
|||
fun `Disclosed poll - ended, some votes, including one from current user (winner)`() = runTest {
|
||||
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||
Truth.assertThat(
|
||||
factory.create(aPollContent(votes = votes, endTime = 1UL), eventId = null)
|
||||
factory.create(
|
||||
aPollContent(votes = votes, endTime = 1UL),
|
||||
eventId = null,
|
||||
eventTimelineItem.isOwn
|
||||
)
|
||||
)
|
||||
.isEqualTo(
|
||||
aTimelineItemPollContent(
|
||||
|
|
@ -113,7 +117,11 @@ internal class TimelineItemContentPollFactoryTest {
|
|||
fun `Disclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest {
|
||||
val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||
Truth.assertThat(
|
||||
factory.create(aPollContent(votes = votes, endTime = 1UL), eventId = null)
|
||||
factory.create(
|
||||
aPollContent(votes = votes, endTime = 1UL),
|
||||
eventId = null,
|
||||
eventTimelineItem.isOwn
|
||||
)
|
||||
)
|
||||
.isEqualTo(
|
||||
aTimelineItemPollContent(
|
||||
|
|
@ -131,7 +139,11 @@ internal class TimelineItemContentPollFactoryTest {
|
|||
@Test
|
||||
fun `Undisclosed poll - not ended, no votes`() = runTest {
|
||||
Truth.assertThat(
|
||||
factory.create(aPollContent(PollKind.Undisclosed).copy(), eventId = null)
|
||||
factory.create(
|
||||
aPollContent(PollKind.Undisclosed).copy(),
|
||||
eventId = null,
|
||||
eventTimelineItem.isOwn
|
||||
)
|
||||
).isEqualTo(
|
||||
aTimelineItemPollContent(pollKind = PollKind.Undisclosed).let {
|
||||
it.copy(answerItems = it.answerItems.map { answerItem -> answerItem.copy(isDisclosed = false) })
|
||||
|
|
@ -143,7 +155,11 @@ internal class TimelineItemContentPollFactoryTest {
|
|||
fun `Undisclosed poll - not ended, some votes, including one from current user`() = runTest {
|
||||
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||
Truth.assertThat(
|
||||
factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes), eventId = null)
|
||||
factory.create(
|
||||
aPollContent(pollKind = PollKind.Undisclosed, votes = votes),
|
||||
eventId = null,
|
||||
eventTimelineItem.isOwn
|
||||
)
|
||||
)
|
||||
.isEqualTo(
|
||||
aTimelineItemPollContent(
|
||||
|
|
@ -161,7 +177,11 @@ internal class TimelineItemContentPollFactoryTest {
|
|||
@Test
|
||||
fun `Undisclosed poll - ended, no votes, no winner`() = runTest {
|
||||
Truth.assertThat(
|
||||
factory.create(aPollContent(pollKind = PollKind.Undisclosed, endTime = 1UL), eventId = null)
|
||||
factory.create(
|
||||
aPollContent(pollKind = PollKind.Undisclosed, endTime = 1UL),
|
||||
eventId = null,
|
||||
eventTimelineItem.isOwn
|
||||
)
|
||||
).isEqualTo(
|
||||
aTimelineItemPollContent().let {
|
||||
it.copy(
|
||||
|
|
@ -179,7 +199,11 @@ internal class TimelineItemContentPollFactoryTest {
|
|||
fun `Undisclosed poll - ended, some votes, including one from current user (winner)`() = runTest {
|
||||
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||
Truth.assertThat(
|
||||
factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes, endTime = 1UL), eventId = null)
|
||||
factory.create(
|
||||
aPollContent(pollKind = PollKind.Undisclosed, votes = votes, endTime = 1UL),
|
||||
eventId = null,
|
||||
eventTimelineItem.isOwn
|
||||
)
|
||||
)
|
||||
.isEqualTo(
|
||||
aTimelineItemPollContent(
|
||||
|
|
@ -199,7 +223,11 @@ internal class TimelineItemContentPollFactoryTest {
|
|||
fun `Undisclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest {
|
||||
val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||
Truth.assertThat(
|
||||
factory.create(aPollContent(PollKind.Undisclosed).copy(votes = votes, endTime = 1UL), eventId = null)
|
||||
factory.create(
|
||||
aPollContent(PollKind.Undisclosed).copy(votes = votes, endTime = 1UL),
|
||||
eventId = null,
|
||||
eventTimelineItem.isOwn
|
||||
)
|
||||
)
|
||||
.isEqualTo(
|
||||
aTimelineItemPollContent(
|
||||
|
|
@ -217,10 +245,14 @@ internal class TimelineItemContentPollFactoryTest {
|
|||
|
||||
@Test
|
||||
fun `eventId is populated`() = runTest {
|
||||
Truth.assertThat(factory.create(aPollContent(), eventId = null))
|
||||
Truth.assertThat(factory.create(aPollContent(), eventId = null, eventTimelineItem.isOwn))
|
||||
.isEqualTo(aTimelineItemPollContent(eventId = null))
|
||||
|
||||
Truth.assertThat(factory.create(aPollContent(), eventId = AN_EVENT_ID))
|
||||
Truth.assertThat(factory.create(
|
||||
aPollContent(),
|
||||
eventId = AN_EVENT_ID,
|
||||
eventTimelineItem.isOwn
|
||||
))
|
||||
.isEqualTo(aTimelineItemPollContent(eventId = AN_EVENT_ID))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.actions
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
||||
interface EndPollAction {
|
||||
suspend fun execute(pollStartId: EventId): Result<Unit>
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.actions
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
||||
interface SendPollResponseAction {
|
||||
suspend fun execute(pollStartId: EventId, answerId: String): Result<Unit>
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.poll.api
|
||||
package io.element.android.features.poll.api.pollcontent
|
||||
|
||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.poll.api
|
||||
package io.element.android.features.poll.api.pollcontent
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.poll.api
|
||||
package io.element.android.features.poll.api.pollcontent
|
||||
|
||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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.pollcontent
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class PollContentState(
|
||||
val eventId: EventId?,
|
||||
val question: String,
|
||||
val answerItems: ImmutableList<PollAnswerItem>,
|
||||
val pollKind: PollKind,
|
||||
val isPollEditable: Boolean,
|
||||
val isPollEnded: Boolean,
|
||||
val isMine: Boolean,
|
||||
)
|
||||
|
|
@ -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.pollcontent
|
||||
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
|
||||
interface PollContentStateFactory {
|
||||
suspend fun create(event: EventTimelineItem, content: PollContent): PollContentState
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.poll.api
|
||||
package io.element.android.features.poll.api.pollcontent
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -49,6 +49,29 @@ import io.element.android.libraries.matrix.api.poll.PollKind
|
|||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun PollContentView(
|
||||
state: PollContentState,
|
||||
onAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit,
|
||||
onPollEdit: (pollStartId: EventId) -> Unit,
|
||||
onPollEnd: (pollStartId: EventId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
PollContentView(
|
||||
eventId = state.eventId,
|
||||
question = state.question,
|
||||
answerItems = state.answerItems,
|
||||
pollKind = state.pollKind,
|
||||
isPollEditable = state.isPollEditable,
|
||||
isPollEnded = state.isPollEnded,
|
||||
isMine = state.isMine,
|
||||
onPollEdit = onPollEdit,
|
||||
onAnswerSelected = onAnswerSelected,
|
||||
onPollEnd = onPollEnd,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PollContentView(
|
||||
eventId: EventId?,
|
||||
|
|
@ -40,6 +40,7 @@ dependencies {
|
|||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(projects.features.messages.api)
|
||||
implementation(projects.libraries.dateformatter.api)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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.actions
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import im.vector.app.features.analytics.plan.PollEnd
|
||||
import io.element.android.features.poll.api.actions.EndPollAction
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(RoomScope::class)
|
||||
class DefaultEndPollAction @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : EndPollAction {
|
||||
|
||||
override suspend fun execute(pollStartId: EventId): Result<Unit> {
|
||||
return room.endPoll(
|
||||
pollStartId = pollStartId,
|
||||
text = "The poll with event id: $pollStartId has ended."
|
||||
).onSuccess {
|
||||
analyticsService.capture(PollEnd())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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.actions
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import im.vector.app.features.analytics.plan.PollVote
|
||||
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(RoomScope::class)
|
||||
class DefaultSendPollResponseAction @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : SendPollResponseAction {
|
||||
|
||||
override suspend fun execute(pollStartId: EventId, answerId: String): Result<Unit> {
|
||||
return room.sendPollResponse(
|
||||
pollStartId = pollStartId,
|
||||
answers = listOf(answerId),
|
||||
).onSuccess {
|
||||
analyticsService.capture(PollVote())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -16,6 +16,11 @@
|
|||
|
||||
package io.element.android.features.poll.impl.history
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
||||
sealed interface PollHistoryEvents {
|
||||
data object History : PollHistoryEvents
|
||||
data object LoadMore : PollHistoryEvents
|
||||
data class PollAnswerSelected(val pollStartId: EventId, val answerId: String) : PollHistoryEvents
|
||||
data class PollEndClicked(val pollStartId: EventId) : PollHistoryEvents
|
||||
data object EditPoll : PollHistoryEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.history
|
||||
|
||||
import io.element.android.features.poll.api.pollcontent.PollContentState
|
||||
|
||||
sealed interface PollHistoryItem {
|
||||
data class PollContent(val formattedDate: String, val state: PollContentState) : PollHistoryItem
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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.history
|
||||
|
||||
import io.element.android.features.poll.api.pollcontent.PollContentStateFactory
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class PollHistoryItemsFactory @Inject constructor(
|
||||
private val pollContentStateFactory: PollContentStateFactory,
|
||||
private val daySeparatorFormatter: DaySeparatorFormatter,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
|
||||
suspend fun create(timelineItems: List<MatrixTimelineItem>): List<PollHistoryItem> = withContext(dispatchers.computation) {
|
||||
timelineItems.mapNotNull { create(it) }.reversed()
|
||||
}
|
||||
|
||||
private suspend fun create(timelineItem: MatrixTimelineItem): PollHistoryItem? {
|
||||
return when (timelineItem) {
|
||||
is MatrixTimelineItem.Event -> {
|
||||
val pollContent = timelineItem.event.content as? PollContent ?: return null
|
||||
val pollContentState = pollContentStateFactory.create(timelineItem.event, pollContent)
|
||||
PollHistoryItem.PollContent(
|
||||
formattedDate = daySeparatorFormatter.format(timelineItem.event.timestamp),
|
||||
state = pollContentState
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -17,22 +17,29 @@
|
|||
package io.element.android.features.poll.impl.history
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.poll.api.actions.EndPollAction
|
||||
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class PollHistoryPresenter @AssistedInject constructor(
|
||||
@Assisted private val pollHistory: MatrixTimeline,
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
private val sendPollResponseAction: SendPollResponseAction,
|
||||
private val endPollAction: EndPollAction,
|
||||
private val pollHistoryItemFactory: PollHistoryItemsFactory,
|
||||
) : Presenter<PollHistoryState> {
|
||||
|
||||
@AssistedFactory
|
||||
|
|
@ -47,26 +54,36 @@ class PollHistoryPresenter @AssistedInject constructor(
|
|||
val paginationState by pollHistory.paginationState.collectAsState()
|
||||
val timelineItemsFlow = remember {
|
||||
pollHistory.timelineItems.map { items ->
|
||||
items.filterIsInstance<MatrixTimelineItem.Event>()
|
||||
.map { it.event.content }
|
||||
.filterIsInstance<PollContent>()
|
||||
.reversed()
|
||||
}.onEach {
|
||||
if (it.isEmpty()) pollHistory.paginateBackwards(20, 50)
|
||||
pollHistoryItemFactory.create(items)
|
||||
}
|
||||
}
|
||||
val items by timelineItemsFlow.collectAsState(initial = emptyList())
|
||||
|
||||
LaunchedEffect(items.size) {
|
||||
if (items.isEmpty()) loadMore()
|
||||
}
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
fun handleEvents(event: PollHistoryEvents) {
|
||||
when (event) {
|
||||
is PollHistoryEvents.History -> Unit // TODO which events to handle?
|
||||
is PollHistoryEvents.LoadMore -> {
|
||||
coroutineScope.loadMore()
|
||||
}
|
||||
is PollHistoryEvents.PollAnswerSelected -> appCoroutineScope.launch {
|
||||
sendPollResponseAction.execute(pollStartId = event.pollStartId, answerId = event.answerId)
|
||||
}
|
||||
is PollHistoryEvents.PollEndClicked -> appCoroutineScope.launch {
|
||||
endPollAction.execute(pollStartId = event.pollStartId)
|
||||
}
|
||||
PollHistoryEvents.EditPoll -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
return PollHistoryState(
|
||||
paginationState = paginationState,
|
||||
matrixTimelineItems = items.toImmutableList(),
|
||||
pollItems = items.toImmutableList(),
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.loadMore() = launch {
|
||||
pollHistory.paginateBackwards(20, 3)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,10 @@
|
|||
package io.element.android.features.poll.impl.history
|
||||
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class PollHistoryState(
|
||||
val paginationState: MatrixTimeline.PaginationState,
|
||||
val matrixTimelineItems: ImmutableList<PollContent>,
|
||||
val pollItems: ImmutableList<PollHistoryItem>,
|
||||
val eventSink: (PollHistoryEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,27 +16,31 @@
|
|||
|
||||
package io.element.android.features.poll.impl.history
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.poll.api.PollAnswerItem
|
||||
import io.element.android.features.poll.api.PollContentView
|
||||
import io.element.android.features.poll.api.pollcontent.PollContentView
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
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.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
|
@ -70,37 +74,75 @@ fun PollHistoryView(
|
|||
if (state.paginationState.isBackPaginating) item {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
items(state.matrixTimelineItems) { pollContent ->
|
||||
PollContentView(
|
||||
eventId = null,
|
||||
question = pollContent.question,
|
||||
answerItems = pollContent.answers.map {
|
||||
PollAnswerItem(
|
||||
answer = PollAnswer(
|
||||
id = it.id,
|
||||
text = it.text,
|
||||
),
|
||||
isSelected = false,
|
||||
isEnabled = false,
|
||||
isWinner = false,
|
||||
isDisclosed = false,
|
||||
votesCount = 9393,
|
||||
percentage = 4.5f,
|
||||
)
|
||||
}.toImmutableList(),
|
||||
pollKind = pollContent.kind,
|
||||
isPollEditable = false,
|
||||
isPollEnded = false,
|
||||
isMine = false,
|
||||
onAnswerSelected = { _, _ -> },
|
||||
onPollEdit = {},
|
||||
onPollEnd = {},
|
||||
itemsIndexed(state.pollItems) { index, pollHistoryItem ->
|
||||
PollHistoryItemRow(
|
||||
pollHistoryItem = pollHistoryItem,
|
||||
onAnswerSelected = fun(pollStartId: EventId, answerId: String) {
|
||||
state.eventSink(PollHistoryEvents.PollAnswerSelected(pollStartId, answerId))
|
||||
},
|
||||
onPollEdit = {
|
||||
state.eventSink(PollHistoryEvents.EditPoll)
|
||||
},
|
||||
onPollEnd = {
|
||||
state.eventSink(PollHistoryEvents.PollEndClicked(it))
|
||||
},
|
||||
)
|
||||
if (index != state.pollItems.lastIndex) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PollHistoryItemRow(
|
||||
pollHistoryItem: PollHistoryItem,
|
||||
onAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit,
|
||||
onPollEdit: (pollStartId: EventId) -> Unit,
|
||||
onPollEnd: (pollStartId: EventId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (pollHistoryItem) {
|
||||
is PollHistoryItem.PollContent -> {
|
||||
PollContentItemRow(
|
||||
pollContentItem = pollHistoryItem,
|
||||
onAnswerSelected = onAnswerSelected,
|
||||
onPollEdit = onPollEdit,
|
||||
onPollEnd = onPollEnd,
|
||||
modifier = modifier.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 24.dp
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PollContentItemRow(
|
||||
pollContentItem: PollHistoryItem.PollContent,
|
||||
onAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit,
|
||||
onPollEdit: (pollStartId: EventId) -> Unit,
|
||||
onPollEnd: (pollStartId: EventId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Text(
|
||||
text = pollContentItem.formattedDate,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
PollContentView(
|
||||
state = pollContentItem.state,
|
||||
onAnswerSelected = onAnswerSelected,
|
||||
onPollEdit = onPollEdit,
|
||||
onPollEnd = onPollEnd,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun PollHistoryViewPreview(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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.model
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.poll.api.pollcontent.PollAnswerItem
|
||||
import io.element.android.features.poll.api.pollcontent.PollContentState
|
||||
import io.element.android.features.poll.api.pollcontent.PollContentStateFactory
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.poll.isDisclosed
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(RoomScope::class)
|
||||
class DefaultPollContentStateFactory @Inject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
) : PollContentStateFactory {
|
||||
|
||||
override suspend fun create(
|
||||
event: EventTimelineItem,
|
||||
content: PollContent
|
||||
): PollContentState {
|
||||
val totalVoteCount = content.votes.flatMap { it.value }.size
|
||||
val myVotes = content.votes.filter { matrixClient.sessionId in it.value }.keys
|
||||
val isPollEnded = content.endTime != null
|
||||
val winnerIds = if (!isPollEnded) {
|
||||
emptyList()
|
||||
} else {
|
||||
content.answers
|
||||
.map { answer -> answer.id }
|
||||
.groupBy { answerId -> content.votes[answerId]?.size ?: 0 } // Group by votes count
|
||||
.maxByOrNull { (votes, _) -> votes } // Keep max voted answers
|
||||
?.takeIf { (votes, _) -> votes > 0 } // Ignore if no option has been voted
|
||||
?.value
|
||||
.orEmpty()
|
||||
}
|
||||
val answerItems = content.answers.map { answer ->
|
||||
val answerVoteCount = content.votes[answer.id]?.size ?: 0
|
||||
val isSelected = answer.id in myVotes
|
||||
val isWinner = answer.id in winnerIds
|
||||
val percentage = if (totalVoteCount > 0) answerVoteCount.toFloat() / totalVoteCount.toFloat() else 0f
|
||||
PollAnswerItem(
|
||||
answer = answer,
|
||||
isSelected = isSelected,
|
||||
isEnabled = !isPollEnded,
|
||||
isWinner = isWinner,
|
||||
isDisclosed = content.kind.isDisclosed || isPollEnded,
|
||||
votesCount = answerVoteCount,
|
||||
percentage = percentage,
|
||||
)
|
||||
}
|
||||
|
||||
return PollContentState(
|
||||
eventId = event.eventId,
|
||||
isMine = event.isOwn,
|
||||
question = content.question,
|
||||
answerItems = answerItems.toImmutableList(),
|
||||
pollKind = content.kind,
|
||||
isPollEditable = event.isEditable,
|
||||
isPollEnded = isPollEnded,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface MatrixTimeline: AutoCloseable {
|
||||
interface MatrixTimeline : AutoCloseable {
|
||||
|
||||
data class PaginationState(
|
||||
val isBackPaginating: Boolean,
|
||||
|
|
@ -35,6 +35,5 @@ interface MatrixTimeline: AutoCloseable {
|
|||
|
||||
suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit>
|
||||
suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit>
|
||||
|
||||
suspend fun sendReadReceipt(eventId: EventId): Result<Unit>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,15 +192,19 @@ class RustMatrixTimeline(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> = withContext(dispatcher) {
|
||||
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> {
|
||||
val paginationOptions = PaginationOptions.UntilNumItems(
|
||||
eventLimit = requestSize.toUShort(),
|
||||
items = untilNumberOfItems.toUShort(),
|
||||
waitForToken = true,
|
||||
)
|
||||
return paginateBackwards(paginationOptions)
|
||||
}
|
||||
|
||||
private suspend fun paginateBackwards(paginationOptions: PaginationOptions): Result<Unit> = withContext(dispatcher) {
|
||||
runCatching {
|
||||
if (!canBackPaginate()) throw TimelineException.CannotPaginate
|
||||
Timber.v("Start back paginating for room ${matrixRoom.roomId} ")
|
||||
val paginationOptions = PaginationOptions.UntilNumItems(
|
||||
eventLimit = requestSize.toUShort(),
|
||||
items = untilNumberOfItems.toUShort(),
|
||||
waitForToken = true,
|
||||
)
|
||||
innerTimeline.paginateBackwards(paginationOptions)
|
||||
}.onFailure { error ->
|
||||
if (error is TimelineException.CannotPaginate) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue