Skip to content

Commit

Permalink
Merge pull request #1913 from element-hq/julioromano/poll_history_ent…
Browse files Browse the repository at this point in the history
…ry_point

Poll history UI
  • Loading branch information
bmarty authored Dec 14, 2023
2 parents cabea13 + dfec3ab commit 7283732
Show file tree
Hide file tree
Showing 231 changed files with 2,470 additions and 747 deletions.
1 change: 1 addition & 0 deletions changelog.d/2014.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Poll history of a room is now accessible from the room details screen.
2 changes: 2 additions & 0 deletions features/messages/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ dependencies {
testImplementation(libs.test.mockk)
testImplementation(libs.test.junitext)
testImplementation(libs.test.robolectric)
testImplementation(projects.features.poll.test)
testImplementation(projects.features.poll.impl)
testImplementation(libs.androidx.compose.ui.test.junit)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -53,7 +53,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
Expand All @@ -70,11 +69,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
Expand Down Expand Up @@ -133,18 +133,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -521,8 +521,6 @@ private fun MessageEventBubbleContent(
) {
TimelineItemEventContentView(
content = event.content,
isMine = event.isMine,
isEditable = event.isEditable,
onLinkClicked = { url ->
when (val permalink = PermalinkParser.parse(Uri.parse(url))) {
is PermalinkData.UserLink -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,6 @@ fun TimelineItemStateEventRow(
) {
TimelineItemEventContentView(
content = event.content,
isMine = event.isMine,
isEditable = event.isEditable,
onLinkClicked = {},
extraPadding = noExtraPadding,
eventSink = eventSink,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ import io.element.android.libraries.architecture.Presenter
@Composable
fun TimelineItemEventContentView(
content: TimelineItemEventContent,
isMine: Boolean,
isEditable: Boolean,
extraPadding: ExtraPadding,
onLinkClicked: (url: String) -> Unit,
eventSink: (TimelineEvents) -> Unit,
Expand Down Expand Up @@ -98,8 +96,6 @@ fun TimelineItemEventContentView(
)
is TimelineItemPollContent -> TimelineItemPollView(
content = content,
isMine = isMine,
isEditable = isEditable,
eventSink = eventSink,
modifier = modifier,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,8 +31,6 @@ import kotlinx.collections.immutable.toImmutableList
@Composable
fun TimelineItemPollView(
content: TimelineItemPollContent,
isMine: Boolean,
isEditable: Boolean,
eventSink: (TimelineEvents) -> Unit,
modifier: Modifier = Modifier,
) {
Expand All @@ -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,
Expand All @@ -69,20 +67,6 @@ internal fun TimelineItemPollViewPreview(@PreviewParameter(TimelineItemPollConte
ElementPreview {
TimelineItemPollView(
content = content,
isMine = false,
isEditable = false,
eventSink = {},
)
}

@PreviewsDayNight
@Composable
internal fun TimelineItemPollCreatorViewPreview(@PreviewParameter(TimelineItemPollContentProvider::class) content: TimelineItemPollContent) =
ElementPreview {
TimelineItemPollView(
content = content,
isMine = true,
isEditable = false,
eventSink = {},
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,64 +19,33 @@ 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,
isEdited = content.isEdited,
isMine = pollContentState.isMine,
isEditable = pollContentState.isPollEditable,
eventId = event.eventId,
question = pollContentState.question,
answerItems = pollContentState.answerItems,
pollKind = pollContentState.pollKind,
isEnded = pollContentState.isPollEnded,
isEdited = content.isEdited
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.PollAnswerItem
import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList
import io.element.android.features.poll.api.pollcontent.aPollQuestion
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.poll.PollKind

Expand All @@ -28,12 +28,16 @@ open class TimelineItemPollContentProvider : PreviewParameterProvider<TimelineIt
get() = sequenceOf(
aTimelineItemPollContent(),
aTimelineItemPollContent().copy(pollKind = PollKind.Undisclosed),
aTimelineItemPollContent().copy(isMine = true),
aTimelineItemPollContent().copy(isMine = true, isEditable = true),
)
}

fun aTimelineItemPollContent(
question: String = aPollQuestion(),
answerItems: List<PollAnswerItem> = aPollAnswerItemList(),
isMine: Boolean = false,
isEditable: Boolean = false,
isEnded: Boolean = false,
isEdited: Boolean = false,
): TimelineItemPollContent {
Expand All @@ -42,6 +46,8 @@ fun aTimelineItemPollContent(
pollKind = PollKind.Disclosed,
question = question,
answerItems = answerItems,
isMine = isMine,
isEditable = isEditable,
isEnded = isEnded,
isEdited = isEdited,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.PollEnd
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
Expand All @@ -48,6 +47,8 @@ import io.element.android.features.messages.impl.voicemessages.timeline.FakeReda
import io.element.android.features.messages.test.FakeMessageComposerContext
import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.features.poll.test.actions.FakeEndPollAction
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
Expand Down Expand Up @@ -91,7 +92,6 @@ import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.consumeItemsUntilTimeout
import io.element.android.tests.testutils.testCoroutineDispatchers
import io.element.android.tests.testutils.waitForPredicate
import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.TestScope
Expand Down Expand Up @@ -619,29 +619,6 @@ class MessagesPresenterTest {
}
}

@Test
fun `present - handle poll end`() = runTest {
val room = FakeMatrixRoom()
val analyticsService = FakeAnalyticsService()
val presenter = createMessagesPresenter(
matrixRoom = room,
analyticsService = analyticsService,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent()))
waitForPredicate { room.endPollInvocations.size == 1 }
cancelAndIgnoreRemainingEvents()
assertThat(room.endPollInvocations.size).isEqualTo(1)
assertThat(room.endPollInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID)
assertThat(room.endPollInvocations.first().text).isEqualTo("The poll with event id: \$anEventId has ended.")
assertThat(analyticsService.capturedEvents.size).isEqualTo(1)
assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollEnd())
}
}

@Test
fun `present - handle action reply to a poll`() = runTest {
val presenter = createMessagesPresenter()
Expand Down Expand Up @@ -706,13 +683,14 @@ class MessagesPresenterTest {
dispatchers = coroutineDispatchers,
appScope = this,
navigator = navigator,
analyticsService = analyticsService,
encryptionService = FakeEncryptionService(),
verificationService = FakeSessionVerificationService(),
featureFlagService = FakeFeatureFlagService(),
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
endPollAction = FakeEndPollAction(),
sendPollResponseAction = FakeSendPollResponseAction(),
)
val timelinePresenterFactory = object: TimelinePresenter.Factory {
val timelinePresenterFactory = object : TimelinePresenter.Factory {
override fun create(navigator: MessagesNavigator): TimelinePresenter {
return timelinePresenter
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package io.element.android.features.messages.impl.fixtures

import io.element.android.features.messages.impl.timeline.aTimelineItemDebugInfo
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
import io.element.android.features.messages.impl.timeline.model.InReplyToDetails
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
Expand All @@ -32,7 +33,6 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.room.aTimelineItemDebugInfo
import kotlinx.collections.immutable.toImmutableList

internal fun aMessageEvent(
Expand Down
Loading

0 comments on commit 7283732

Please sign in to comment.