Browse Source

Polls: share logic around PollContent

pull/1913/head
ganfra 10 months ago
parent
commit
4a2cbb1ed4
  1. 17
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
  2. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
  3. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt
  4. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt
  5. 12
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt
  6. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt
  7. 55
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt
  8. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt
  9. 12
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt
  10. 2
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt
  11. 58
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactoryTest.kt
  12. 23
      features/poll/api/src/main/kotlin/io/element/android/features/poll/api/actions/EndPollAction.kt
  13. 23
      features/poll/api/src/main/kotlin/io/element/android/features/poll/api/actions/SendPollResponseAction.kt
  14. 2
      features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerItem.kt
  15. 2
      features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerView.kt
  16. 2
      features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerViewProvider.kt
  17. 31
      features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentState.kt
  18. 24
      features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentStateFactory.kt
  19. 25
      features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentView.kt
  20. 1
      features/poll/impl/build.gradle.kts
  21. 42
      features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultEndPollAction.kt
  22. 43
      features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultSendPollResponseAction.kt
  23. 7
      features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryEvents.kt
  24. 23
      features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryItem.kt
  25. 51
      features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryItemsFactory.kt
  26. 43
      features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt
  27. 3
      features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryState.kt
  28. 102
      features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryView.kt
  29. 80
      features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/model/DefaultPollContentStateFactory.kt
  30. 3
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt
  31. 16
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt

17
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt

@ -30,14 +30,14 @@ import androidx.compose.runtime.saveable.rememberSaveable @@ -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 @@ -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( @@ -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( @@ -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)

4
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt

@ -517,12 +517,10 @@ private fun MessageEventBubbleContent( @@ -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,
)

4
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt

@ -80,12 +80,10 @@ fun TimelineItemStateEventRow( @@ -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()
)

4
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt

@ -41,8 +41,6 @@ import io.element.android.libraries.architecture.Presenter @@ -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( @@ -103,8 +101,6 @@ fun TimelineItemEventContentView(
)
is TimelineItemPollContent -> TimelineItemPollView(
content = content,
isMine = isMine,
isEditable = isEditable,
eventSink = eventSink,
modifier = modifier,
)

12
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt

@ -22,7 +22,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter @@ -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 @@ -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( @@ -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 @@ -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 @@ -81,8 +77,6 @@ internal fun TimelineItemPollCreatorViewPreview(@PreviewParameter(TimelineItemPo
ElementPreview {
TimelineItemPollView(
content = content,
isMine = true,
isEditable = false,
eventSink = {},
)
}

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt

@ -59,7 +59,7 @@ class TimelineItemContentFactory @Inject constructor( @@ -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
}

55
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt

@ -19,63 +19,32 @@ package io.element.android.features.messages.impl.timeline.factories.event @@ -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,
)
}
}

4
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt

@ -16,11 +16,13 @@ @@ -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>,

12
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt

@ -17,9 +17,9 @@ @@ -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 @@ -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( @@ -41,6 +45,8 @@ fun aTimelineItemPollContent(
pollKind = PollKind.Disclosed,
question = question,
answerItems = answerItems,
isMine = isMine,
isEditable = isEditable,
isEnded = isEnded,
)
}

2
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt

@ -29,7 +29,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI @@ -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

58
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactoryTest.kt

@ -18,7 +18,7 @@ package io.element.android.features.messages.impl.timeline.factories.event @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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))
}

23
features/poll/api/src/main/kotlin/io/element/android/features/poll/api/actions/EndPollAction.kt

@ -0,0 +1,23 @@ @@ -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>
}

23
features/poll/api/src/main/kotlin/io/element/android/features/poll/api/actions/SendPollResponseAction.kt

@ -0,0 +1,23 @@ @@ -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>
}

2
features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerItem.kt → features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerItem.kt

@ -14,7 +14,7 @@ @@ -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

2
features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt → features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerView.kt

@ -14,7 +14,7 @@ @@ -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

2
features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt → features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerViewProvider.kt

@ -14,7 +14,7 @@ @@ -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

31
features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentState.kt

@ -0,0 +1,31 @@ @@ -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,
)

24
features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentStateFactory.kt

@ -0,0 +1,24 @@ @@ -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
}

25
features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt → features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentView.kt

@ -14,7 +14,7 @@ @@ -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 @@ -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?,

1
features/poll/impl/build.gradle.kts

@ -40,6 +40,7 @@ dependencies { @@ -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)

42
features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultEndPollAction.kt

@ -0,0 +1,42 @@ @@ -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())
}
}
}

43
features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/actions/DefaultSendPollResponseAction.kt

@ -0,0 +1,43 @@ @@ -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())
}
}
}

7
features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryEvents.kt

@ -16,6 +16,11 @@ @@ -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
}

23
features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryItem.kt

@ -0,0 +1,23 @@ @@ -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
}

51
features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryItemsFactory.kt

@ -0,0 +1,51 @@ @@ -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
}
}
}

43
features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt

@ -17,22 +17,29 @@ @@ -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( @@ -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)
}
}

3
features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryState.kt

@ -17,11 +17,10 @@ @@ -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,
)

102
features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryView.kt

@ -16,27 +16,31 @@ @@ -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( @@ -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(

80
features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/model/DefaultPollContentStateFactory.kt

@ -0,0 +1,80 @@ @@ -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,
)
}
}

3
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt

@ -20,7 +20,7 @@ import io.element.android.libraries.matrix.api.core.EventId @@ -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 { @@ -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>
}

16
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt

@ -192,15 +192,19 @@ class RustMatrixTimeline( @@ -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…
Cancel
Save