Polls: share logic around PollContent
This commit is contained in:
parent
61b623041b
commit
67ec58a4f4
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.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
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.MessagesNavigator
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
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.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
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.timeline.session.SessionState
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
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.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
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.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
|
@ -69,11 +68,12 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
private val appScope: CoroutineScope,
|
private val appScope: CoroutineScope,
|
||||||
@Assisted private val navigator: MessagesNavigator,
|
@Assisted private val navigator: MessagesNavigator,
|
||||||
private val analyticsService: AnalyticsService,
|
|
||||||
private val verificationService: SessionVerificationService,
|
private val verificationService: SessionVerificationService,
|
||||||
private val encryptionService: EncryptionService,
|
private val encryptionService: EncryptionService,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
private val redactedVoiceMessageManager: RedactedVoiceMessageManager,
|
private val redactedVoiceMessageManager: RedactedVoiceMessageManager,
|
||||||
|
private val sendPollResponseAction: SendPollResponseAction,
|
||||||
|
private val endPollAction: EndPollAction,
|
||||||
) : Presenter<TimelineState> {
|
) : Presenter<TimelineState> {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
@ -132,18 +132,15 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is TimelineEvents.PollAnswerSelected -> appScope.launch {
|
is TimelineEvents.PollAnswerSelected -> appScope.launch {
|
||||||
room.sendPollResponse(
|
sendPollResponseAction.execute(
|
||||||
pollStartId = event.pollStartId,
|
pollStartId = event.pollStartId,
|
||||||
answers = listOf(event.answerId),
|
answerId = event.answerId
|
||||||
)
|
)
|
||||||
analyticsService.capture(PollVote())
|
|
||||||
}
|
}
|
||||||
is TimelineEvents.PollEndClicked -> appScope.launch {
|
is TimelineEvents.PollEndClicked -> appScope.launch {
|
||||||
room.endPoll(
|
endPollAction.execute(
|
||||||
pollStartId = event.pollStartId,
|
pollStartId = event.pollStartId,
|
||||||
text = "The poll with event id: ${event.pollStartId} has ended."
|
|
||||||
)
|
)
|
||||||
analyticsService.capture(PollEnd())
|
|
||||||
}
|
}
|
||||||
is TimelineEvents.PollEditClicked ->
|
is TimelineEvents.PollEditClicked ->
|
||||||
navigator.onEditPollClicked(event.pollStartId)
|
navigator.onEditPollClicked(event.pollStartId)
|
||||||
|
|
|
||||||
|
|
@ -517,12 +517,10 @@ private fun MessageEventBubbleContent(
|
||||||
) {
|
) {
|
||||||
TimelineItemEventContentView(
|
TimelineItemEventContentView(
|
||||||
content = event.content,
|
content = event.content,
|
||||||
isMine = event.isMine,
|
|
||||||
isEditable = event.isEditable,
|
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
extraPadding = event.toExtraPadding(),
|
||||||
onClick = onMessageClick,
|
onClick = onMessageClick,
|
||||||
onLongClick = onMessageLongClick,
|
onLongClick = onMessageLongClick,
|
||||||
extraPadding = event.toExtraPadding(),
|
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
modifier = contentModifier,
|
modifier = contentModifier,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -80,12 +80,10 @@ fun TimelineItemStateEventRow(
|
||||||
) {
|
) {
|
||||||
TimelineItemEventContentView(
|
TimelineItemEventContentView(
|
||||||
content = event.content,
|
content = event.content,
|
||||||
isMine = event.isMine,
|
|
||||||
isEditable = event.isEditable,
|
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
extraPadding = noExtraPadding,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
extraPadding = noExtraPadding,
|
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
modifier = Modifier.defaultTimelineContentPadding()
|
modifier = Modifier.defaultTimelineContentPadding()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,6 @@ import io.element.android.libraries.architecture.Presenter
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemEventContentView(
|
fun TimelineItemEventContentView(
|
||||||
content: TimelineItemEventContent,
|
content: TimelineItemEventContent,
|
||||||
isMine: Boolean,
|
|
||||||
isEditable: Boolean,
|
|
||||||
interactionSource: MutableInteractionSource,
|
interactionSource: MutableInteractionSource,
|
||||||
extraPadding: ExtraPadding,
|
extraPadding: ExtraPadding,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
|
@ -103,8 +101,6 @@ fun TimelineItemEventContentView(
|
||||||
)
|
)
|
||||||
is TimelineItemPollContent -> TimelineItemPollView(
|
is TimelineItemPollContent -> TimelineItemPollView(
|
||||||
content = content,
|
content = content,
|
||||||
isMine = isMine,
|
|
||||||
isEditable = isEditable,
|
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
modifier = modifier,
|
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.TimelineEvents
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
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.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.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
@ -31,8 +31,6 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemPollView(
|
fun TimelineItemPollView(
|
||||||
content: TimelineItemPollContent,
|
content: TimelineItemPollContent,
|
||||||
isMine: Boolean,
|
|
||||||
isEditable: Boolean,
|
|
||||||
eventSink: (TimelineEvents) -> Unit,
|
eventSink: (TimelineEvents) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -54,8 +52,8 @@ fun TimelineItemPollView(
|
||||||
answerItems = content.answerItems.toImmutableList(),
|
answerItems = content.answerItems.toImmutableList(),
|
||||||
pollKind = content.pollKind,
|
pollKind = content.pollKind,
|
||||||
isPollEnded = content.isEnded,
|
isPollEnded = content.isEnded,
|
||||||
isPollEditable = isEditable,
|
isPollEditable = content.isEditable,
|
||||||
isMine = isMine,
|
isMine = content.isMine,
|
||||||
onAnswerSelected = ::onAnswerSelected,
|
onAnswerSelected = ::onAnswerSelected,
|
||||||
onPollEdit = ::onPollEdit,
|
onPollEdit = ::onPollEdit,
|
||||||
onPollEnd = ::onPollEnd,
|
onPollEnd = ::onPollEnd,
|
||||||
|
|
@ -69,8 +67,6 @@ internal fun TimelineItemPollViewPreview(@PreviewParameter(TimelineItemPollConte
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
TimelineItemPollView(
|
TimelineItemPollView(
|
||||||
content = content,
|
content = content,
|
||||||
isMine = false,
|
|
||||||
isEditable = false,
|
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -81,8 +77,6 @@ internal fun TimelineItemPollCreatorViewPreview(@PreviewParameter(TimelineItemPo
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
TimelineItemPollView(
|
TimelineItemPollView(
|
||||||
content = content,
|
content = content,
|
||||||
isMine = true,
|
|
||||||
isEditable = false,
|
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ class TimelineItemContentFactory @Inject constructor(
|
||||||
is RoomMembershipContent -> roomMembershipFactory.create(eventTimelineItem)
|
is RoomMembershipContent -> roomMembershipFactory.create(eventTimelineItem)
|
||||||
is StateContent -> stateFactory.create(eventTimelineItem)
|
is StateContent -> stateFactory.create(eventTimelineItem)
|
||||||
is StickerContent -> stickerFactory.create(itemContent)
|
is StickerContent -> stickerFactory.create(itemContent)
|
||||||
is PollContent -> pollFactory.create(itemContent, eventTimelineItem.eventId)
|
is PollContent -> pollFactory.create(eventTimelineItem, itemContent)
|
||||||
is UnableToDecryptContent -> utdFactory.create(itemContent)
|
is UnableToDecryptContent -> utdFactory.create(itemContent)
|
||||||
is UnknownContent -> TimelineItemUnknownContent
|
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.TimelineItemEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
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.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.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||||
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.PollContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TimelineItemContentPollFactory @Inject constructor(
|
class TimelineItemContentPollFactory @Inject constructor(
|
||||||
private val matrixClient: MatrixClient,
|
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val pollContentStateFactory: PollContentStateFactory,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun create(
|
suspend fun create(
|
||||||
|
event: EventTimelineItem,
|
||||||
content: PollContent,
|
content: PollContent,
|
||||||
eventId: EventId?
|
|
||||||
): TimelineItemEventContent {
|
): TimelineItemEventContent {
|
||||||
if (!featureFlagService.isFeatureEnabled(FeatureFlags.Polls)) return TimelineItemUnknownContent
|
if (!featureFlagService.isFeatureEnabled(FeatureFlags.Polls)) return TimelineItemUnknownContent
|
||||||
|
val pollContentState = pollContentStateFactory.create(event, content)
|
||||||
// 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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return TimelineItemPollContent(
|
return TimelineItemPollContent(
|
||||||
eventId = eventId,
|
isMine = pollContentState.isMine,
|
||||||
question = content.question,
|
isEditable = pollContentState.isPollEditable,
|
||||||
answerItems = answerItems,
|
eventId = event.eventId,
|
||||||
pollKind = content.kind,
|
question = pollContentState.question,
|
||||||
isEnded = isEndedPoll,
|
answerItems = pollContentState.answerItems,
|
||||||
|
pollKind = pollContentState.pollKind,
|
||||||
|
isEnded = pollContentState.isPollEnded,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,13 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.model.event
|
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.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
|
|
||||||
data class TimelineItemPollContent(
|
data class TimelineItemPollContent(
|
||||||
|
val isMine: Boolean,
|
||||||
|
val isEditable: Boolean,
|
||||||
val eventId: EventId?,
|
val eventId: EventId?,
|
||||||
val question: String,
|
val question: String,
|
||||||
val answerItems: List<PollAnswerItem>,
|
val answerItems: List<PollAnswerItem>,
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@
|
||||||
package io.element.android.features.messages.impl.timeline.model.event
|
package io.element.android.features.messages.impl.timeline.model.event
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.poll.api.PollAnswerItem
|
import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList
|
||||||
import io.element.android.features.poll.api.aPollAnswerItemList
|
import io.element.android.features.poll.api.pollcontent.aPollQuestion
|
||||||
import io.element.android.features.poll.api.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.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
|
|
||||||
|
|
@ -28,12 +28,16 @@ open class TimelineItemPollContentProvider : PreviewParameterProvider<TimelineIt
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aTimelineItemPollContent(),
|
aTimelineItemPollContent(),
|
||||||
aTimelineItemPollContent().copy(pollKind = PollKind.Undisclosed),
|
aTimelineItemPollContent().copy(pollKind = PollKind.Undisclosed),
|
||||||
|
aTimelineItemPollContent().copy(isMine = true),
|
||||||
|
aTimelineItemPollContent().copy(isEditable = true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aTimelineItemPollContent(
|
fun aTimelineItemPollContent(
|
||||||
question: String = aPollQuestion(),
|
question: String = aPollQuestion(),
|
||||||
answerItems: List<PollAnswerItem> = aPollAnswerItemList(),
|
answerItems: List<PollAnswerItem> = aPollAnswerItemList(),
|
||||||
|
isMine: Boolean = false,
|
||||||
|
isEditable: Boolean = false,
|
||||||
isEnded: Boolean = false,
|
isEnded: Boolean = false,
|
||||||
): TimelineItemPollContent {
|
): TimelineItemPollContent {
|
||||||
return TimelineItemPollContent(
|
return TimelineItemPollContent(
|
||||||
|
|
@ -41,6 +45,8 @@ fun aTimelineItemPollContent(
|
||||||
pollKind = PollKind.Disclosed,
|
pollKind = PollKind.Disclosed,
|
||||||
question = question,
|
question = question,
|
||||||
answerItems = answerItems,
|
answerItems = answerItems,
|
||||||
|
isMine = isMine,
|
||||||
|
isEditable = isEditable,
|
||||||
isEnded = isEnded,
|
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.aTimelineItemPollContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
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.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.featureflag.test.InMemoryPreferencesStore
|
||||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
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 com.google.common.truth.Truth
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
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.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
@ -55,14 +55,14 @@ internal class TimelineItemContentPollFactoryTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Disclosed poll - not ended, no votes`() = runTest {
|
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
|
@Test
|
||||||
fun `Disclosed poll - not ended, some votes, including one from current user`() = runTest {
|
fun `Disclosed poll - not ended, some votes, including one from current user`() = runTest {
|
||||||
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||||
Truth.assertThat(
|
Truth.assertThat(
|
||||||
factory.create(aPollContent(votes = votes), eventId = null)
|
factory.create(aPollContent(votes = votes), eventId = null, eventTimelineItem.isOwn)
|
||||||
)
|
)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
aTimelineItemPollContent(
|
aTimelineItemPollContent(
|
||||||
|
|
@ -79,7 +79,7 @@ internal class TimelineItemContentPollFactoryTest {
|
||||||
@Test
|
@Test
|
||||||
fun `Disclosed poll - ended, no votes, no winner`() = runTest {
|
fun `Disclosed poll - ended, no votes, no winner`() = runTest {
|
||||||
Truth.assertThat(
|
Truth.assertThat(
|
||||||
factory.create(aPollContent(endTime = 1UL), eventId = null)
|
factory.create(aPollContent(endTime = 1UL), eventId = null, eventTimelineItem.isOwn)
|
||||||
).isEqualTo(
|
).isEqualTo(
|
||||||
aTimelineItemPollContent().let {
|
aTimelineItemPollContent().let {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
|
@ -94,7 +94,11 @@ internal class TimelineItemContentPollFactoryTest {
|
||||||
fun `Disclosed poll - ended, some votes, including one from current user (winner)`() = runTest {
|
fun `Disclosed poll - ended, some votes, including one from current user (winner)`() = runTest {
|
||||||
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||||
Truth.assertThat(
|
Truth.assertThat(
|
||||||
factory.create(aPollContent(votes = votes, endTime = 1UL), eventId = null)
|
factory.create(
|
||||||
|
aPollContent(votes = votes, endTime = 1UL),
|
||||||
|
eventId = null,
|
||||||
|
eventTimelineItem.isOwn
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
aTimelineItemPollContent(
|
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 {
|
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()
|
val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||||
Truth.assertThat(
|
Truth.assertThat(
|
||||||
factory.create(aPollContent(votes = votes, endTime = 1UL), eventId = null)
|
factory.create(
|
||||||
|
aPollContent(votes = votes, endTime = 1UL),
|
||||||
|
eventId = null,
|
||||||
|
eventTimelineItem.isOwn
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
aTimelineItemPollContent(
|
aTimelineItemPollContent(
|
||||||
|
|
@ -131,7 +139,11 @@ internal class TimelineItemContentPollFactoryTest {
|
||||||
@Test
|
@Test
|
||||||
fun `Undisclosed poll - not ended, no votes`() = runTest {
|
fun `Undisclosed poll - not ended, no votes`() = runTest {
|
||||||
Truth.assertThat(
|
Truth.assertThat(
|
||||||
factory.create(aPollContent(PollKind.Undisclosed).copy(), eventId = null)
|
factory.create(
|
||||||
|
aPollContent(PollKind.Undisclosed).copy(),
|
||||||
|
eventId = null,
|
||||||
|
eventTimelineItem.isOwn
|
||||||
|
)
|
||||||
).isEqualTo(
|
).isEqualTo(
|
||||||
aTimelineItemPollContent(pollKind = PollKind.Undisclosed).let {
|
aTimelineItemPollContent(pollKind = PollKind.Undisclosed).let {
|
||||||
it.copy(answerItems = it.answerItems.map { answerItem -> answerItem.copy(isDisclosed = false) })
|
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 {
|
fun `Undisclosed poll - not ended, some votes, including one from current user`() = runTest {
|
||||||
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||||
Truth.assertThat(
|
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(
|
.isEqualTo(
|
||||||
aTimelineItemPollContent(
|
aTimelineItemPollContent(
|
||||||
|
|
@ -161,7 +177,11 @@ internal class TimelineItemContentPollFactoryTest {
|
||||||
@Test
|
@Test
|
||||||
fun `Undisclosed poll - ended, no votes, no winner`() = runTest {
|
fun `Undisclosed poll - ended, no votes, no winner`() = runTest {
|
||||||
Truth.assertThat(
|
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(
|
).isEqualTo(
|
||||||
aTimelineItemPollContent().let {
|
aTimelineItemPollContent().let {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
|
@ -179,7 +199,11 @@ internal class TimelineItemContentPollFactoryTest {
|
||||||
fun `Undisclosed poll - ended, some votes, including one from current user (winner)`() = runTest {
|
fun `Undisclosed poll - ended, some votes, including one from current user (winner)`() = runTest {
|
||||||
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||||
Truth.assertThat(
|
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(
|
.isEqualTo(
|
||||||
aTimelineItemPollContent(
|
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 {
|
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()
|
val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||||
Truth.assertThat(
|
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(
|
.isEqualTo(
|
||||||
aTimelineItemPollContent(
|
aTimelineItemPollContent(
|
||||||
|
|
@ -217,10 +245,14 @@ internal class TimelineItemContentPollFactoryTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `eventId is populated`() = runTest {
|
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))
|
.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))
|
.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.
|
* 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 io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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 io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
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.
|
* 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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
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 io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
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
|
@Composable
|
||||||
fun PollContentView(
|
fun PollContentView(
|
||||||
eventId: EventId?,
|
eventId: EventId?,
|
||||||
|
|
@ -40,6 +40,7 @@ dependencies {
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(projects.features.messages.api)
|
implementation(projects.features.messages.api)
|
||||||
|
implementation(projects.libraries.dateformatter.api)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
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
|
package io.element.android.features.poll.impl.history
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
||||||
sealed interface PollHistoryEvents {
|
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
|
package io.element.android.features.poll.impl.history
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
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.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
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.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class PollHistoryPresenter @AssistedInject constructor(
|
class PollHistoryPresenter @AssistedInject constructor(
|
||||||
@Assisted private val pollHistory: MatrixTimeline,
|
@Assisted private val pollHistory: MatrixTimeline,
|
||||||
|
private val appCoroutineScope: CoroutineScope,
|
||||||
|
private val sendPollResponseAction: SendPollResponseAction,
|
||||||
|
private val endPollAction: EndPollAction,
|
||||||
|
private val pollHistoryItemFactory: PollHistoryItemsFactory,
|
||||||
) : Presenter<PollHistoryState> {
|
) : Presenter<PollHistoryState> {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
@ -47,26 +54,36 @@ class PollHistoryPresenter @AssistedInject constructor(
|
||||||
val paginationState by pollHistory.paginationState.collectAsState()
|
val paginationState by pollHistory.paginationState.collectAsState()
|
||||||
val timelineItemsFlow = remember {
|
val timelineItemsFlow = remember {
|
||||||
pollHistory.timelineItems.map { items ->
|
pollHistory.timelineItems.map { items ->
|
||||||
items.filterIsInstance<MatrixTimelineItem.Event>()
|
pollHistoryItemFactory.create(items)
|
||||||
.map { it.event.content }
|
|
||||||
.filterIsInstance<PollContent>()
|
|
||||||
.reversed()
|
|
||||||
}.onEach {
|
|
||||||
if (it.isEmpty()) pollHistory.paginateBackwards(20, 50)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val items by timelineItemsFlow.collectAsState(initial = emptyList())
|
val items by timelineItemsFlow.collectAsState(initial = emptyList())
|
||||||
|
LaunchedEffect(items.size) {
|
||||||
|
if (items.isEmpty()) loadMore()
|
||||||
|
}
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
fun handleEvents(event: PollHistoryEvents) {
|
fun handleEvents(event: PollHistoryEvents) {
|
||||||
when (event) {
|
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(
|
return PollHistoryState(
|
||||||
paginationState = paginationState,
|
paginationState = paginationState,
|
||||||
matrixTimelineItems = items.toImmutableList(),
|
pollItems = items.toImmutableList(),
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.loadMore() = launch {
|
||||||
|
pollHistory.paginateBackwards(20, 3)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,10 @@
|
||||||
package io.element.android.features.poll.impl.history
|
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.MatrixTimeline
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class PollHistoryState(
|
data class PollHistoryState(
|
||||||
val paginationState: MatrixTimeline.PaginationState,
|
val paginationState: MatrixTimeline.PaginationState,
|
||||||
val matrixTimelineItems: ImmutableList<PollContent>,
|
val pollItems: ImmutableList<PollHistoryItem>,
|
||||||
val eventSink: (PollHistoryEvents) -> Unit,
|
val eventSink: (PollHistoryEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,27 +16,31 @@
|
||||||
|
|
||||||
package io.element.android.features.poll.impl.history
|
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.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
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.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.poll.api.PollAnswerItem
|
import io.element.android.features.poll.api.pollcontent.PollContentView
|
||||||
import io.element.android.features.poll.api.PollContentView
|
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
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.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.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -70,37 +74,75 @@ fun PollHistoryView(
|
||||||
if (state.paginationState.isBackPaginating) item {
|
if (state.paginationState.isBackPaginating) item {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
items(state.matrixTimelineItems) { pollContent ->
|
itemsIndexed(state.pollItems) { index, pollHistoryItem ->
|
||||||
PollContentView(
|
PollHistoryItemRow(
|
||||||
eventId = null,
|
pollHistoryItem = pollHistoryItem,
|
||||||
question = pollContent.question,
|
onAnswerSelected = fun(pollStartId: EventId, answerId: String) {
|
||||||
answerItems = pollContent.answers.map {
|
state.eventSink(PollHistoryEvents.PollAnswerSelected(pollStartId, answerId))
|
||||||
PollAnswerItem(
|
},
|
||||||
answer = PollAnswer(
|
onPollEdit = {
|
||||||
id = it.id,
|
state.eventSink(PollHistoryEvents.EditPoll)
|
||||||
text = it.text,
|
},
|
||||||
),
|
onPollEnd = {
|
||||||
isSelected = false,
|
state.eventSink(PollHistoryEvents.PollEndClicked(it))
|
||||||
isEnabled = false,
|
},
|
||||||
isWinner = false,
|
|
||||||
isDisclosed = false,
|
|
||||||
votesCount = 9393,
|
|
||||||
percentage = 4.5f,
|
|
||||||
)
|
|
||||||
}.toImmutableList(),
|
|
||||||
pollKind = pollContent.kind,
|
|
||||||
isPollEditable = false,
|
|
||||||
isPollEnded = false,
|
|
||||||
isMine = false,
|
|
||||||
onAnswerSelected = { _, _ -> },
|
|
||||||
onPollEdit = {},
|
|
||||||
onPollEnd = {},
|
|
||||||
)
|
)
|
||||||
|
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
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollHistoryViewPreview(
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
interface MatrixTimeline: AutoCloseable {
|
interface MatrixTimeline : AutoCloseable {
|
||||||
|
|
||||||
data class PaginationState(
|
data class PaginationState(
|
||||||
val isBackPaginating: Boolean,
|
val isBackPaginating: Boolean,
|
||||||
|
|
@ -35,6 +35,5 @@ interface MatrixTimeline: AutoCloseable {
|
||||||
|
|
||||||
suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit>
|
suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit>
|
||||||
suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit>
|
suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit>
|
||||||
|
|
||||||
suspend fun sendReadReceipt(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 {
|
runCatching {
|
||||||
if (!canBackPaginate()) throw TimelineException.CannotPaginate
|
if (!canBackPaginate()) throw TimelineException.CannotPaginate
|
||||||
Timber.v("Start back paginating for room ${matrixRoom.roomId} ")
|
Timber.v("Start back paginating for room ${matrixRoom.roomId} ")
|
||||||
val paginationOptions = PaginationOptions.UntilNumItems(
|
|
||||||
eventLimit = requestSize.toUShort(),
|
|
||||||
items = untilNumberOfItems.toUShort(),
|
|
||||||
waitForToken = true,
|
|
||||||
)
|
|
||||||
innerTimeline.paginateBackwards(paginationOptions)
|
innerTimeline.paginateBackwards(paginationOptions)
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
if (error is TimelineException.CannotPaginate) {
|
if (error is TimelineException.CannotPaginate) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue