Merge branch 'develop' of https://github.com/vector-im/element-x-android into feature/dla/emojibase_integration

This commit is contained in:
David Langley 2023-08-30 12:48:38 +01:00
commit 5aeff965b1
73 changed files with 1608 additions and 253 deletions

View file

@ -46,6 +46,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.features.poll.api.create.CreatePollEntryPoint
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode
@ -64,6 +65,7 @@ class MessagesFlowNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>,
private val sendLocationEntryPoint: SendLocationEntryPoint,
private val showLocationEntryPoint: ShowLocationEntryPoint,
private val createPollEntryPoint: CreatePollEntryPoint,
) : BackstackNode<MessagesFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Messages,
@ -101,6 +103,9 @@ class MessagesFlowNode @AssistedInject constructor(
@Parcelize
data object SendLocation : NavTarget
@Parcelize
data object CreatePoll : NavTarget
}
private val callback = plugins<MessagesEntryPoint.Callback>().firstOrNull()
@ -140,6 +145,10 @@ class MessagesFlowNode @AssistedInject constructor(
override fun onSendLocationClicked() {
backstack.push(NavTarget.SendLocation)
}
override fun onCreatePollClicked() {
backstack.push(NavTarget.CreatePoll)
}
}
createNode<MessagesNode>(buildContext, listOf(callback))
}
@ -179,6 +188,9 @@ class MessagesFlowNode @AssistedInject constructor(
NavTarget.SendLocation -> {
sendLocationEntryPoint.createNode(this, buildContext)
}
NavTarget.CreatePoll -> {
createPollEntryPoint.createNode(this, buildContext)
}
}
}

View file

@ -58,6 +58,7 @@ class MessagesNode @AssistedInject constructor(
fun onForwardEventClicked(eventId: EventId)
fun onReportMessage(eventId: EventId, senderId: UserId)
fun onSendLocationClicked()
fun onCreatePollClicked()
}
init {
@ -99,6 +100,10 @@ class MessagesNode @AssistedInject constructor(
callback?.onSendLocationClicked()
}
private fun onCreatePollClicked() {
callback?.onCreatePollClicked()
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
@ -110,6 +115,7 @@ class MessagesNode @AssistedInject constructor(
onPreviewAttachments = this::onPreviewAttachments,
onUserDataClicked = this::onUserDataClicked,
onSendLocationClicked = this::onSendLocationClicked,
onCreatePollClicked = this::onCreatePollClicked,
modifier = modifier,
)
}

View file

@ -97,6 +97,7 @@ fun MessagesView(
onUserDataClicked: (UserId) -> Unit,
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit,
onSendLocationClicked: () -> Unit,
onCreatePollClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
LogCompositions(tag = "MessagesScreen", msg = "Root")
@ -175,6 +176,7 @@ fun MessagesView(
onReactionLongClicked = ::onEmojiReactionLongClicked,
onMoreReactionsClicked = ::onMoreReactionsClicked,
onSendLocationClicked = onSendLocationClicked,
onCreatePollClicked = onCreatePollClicked,
onSwipeToReply = { targetEvent ->
state.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, targetEvent))
},
@ -266,6 +268,7 @@ private fun MessagesViewContent(
onMessageLongClicked: (TimelineItem.Event) -> Unit,
onTimestampClicked: (TimelineItem.Event) -> Unit,
onSendLocationClicked: () -> Unit,
onCreatePollClicked: () -> Unit,
modifier: Modifier = Modifier,
onSwipeToReply: (TimelineItem.Event) -> Unit,
) {
@ -294,6 +297,7 @@ private fun MessagesViewContent(
MessageComposerView(
state = state.composerState,
onSendLocationClicked = onSendLocationClicked,
onCreatePollClicked = onCreatePollClicked,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(Alignment.Bottom)
@ -400,5 +404,6 @@ private fun ContentToPreview(state: MessagesState) {
onPreviewAttachments = {},
onUserDataClicked = {},
onSendLocationClicked = {},
onCreatePollClicked = {},
)
}

View file

@ -25,6 +25,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
import io.element.android.features.messages.impl.timeline.model.event.canBeCopied
@ -96,6 +97,22 @@ class ActionListPresenter @Inject constructor(
}
}
}
is TimelineItemPollContent -> {
buildList {
if (timelineItem.content.canBeCopied()) {
add(TimelineItemAction.Copy)
}
if (buildMeta.isDebuggable) {
add(TimelineItemAction.Developer)
}
if (!timelineItem.isMine) {
add(TimelineItemAction.ReportContent)
}
if (timelineItem.isMine || userCanRedact) {
add(TimelineItemAction.Redact)
}
}
}
else -> buildList<TimelineItemAction> {
if (timelineItem.isRemote) {
// Can only reply or forward messages already uploaded to the server

View file

@ -24,6 +24,7 @@ import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ListItem
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AttachFile
import androidx.compose.material.icons.filled.BarChart
import androidx.compose.material.icons.filled.Collections
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.PhotoCamera
@ -52,6 +53,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
internal fun AttachmentsBottomSheet(
state: MessageComposerState,
onSendLocationClicked: () -> Unit,
onCreatePollClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
val localView = LocalView.current
@ -85,6 +87,7 @@ internal fun AttachmentsBottomSheet(
AttachmentSourcePickerMenu(
state = state,
onSendLocationClicked = onSendLocationClicked,
onCreatePollClicked = onCreatePollClicked,
)
}
}
@ -95,6 +98,7 @@ internal fun AttachmentsBottomSheet(
internal fun AttachmentSourcePickerMenu(
state: MessageComposerState,
onSendLocationClicked: () -> Unit,
onCreatePollClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
@ -131,6 +135,16 @@ internal fun AttachmentSourcePickerMenu(
text = { Text(stringResource(R.string.screen_room_attachment_source_location)) },
)
}
if (state.canCreatePoll) {
ListItem(
modifier = Modifier.clickable {
state.eventSink(MessageComposerEvents.PickAttachmentSource.Poll)
onCreatePollClicked()
},
icon = { Icon(Icons.Default.BarChart, null) },
text = { Text(stringResource(R.string.screen_room_attachment_source_poll)) },
)
}
}
}
@ -142,5 +156,6 @@ internal fun AttachmentSourcePickerMenuPreview() = ElementPreview {
canShareLocation = true,
),
onSendLocationClicked = {},
onCreatePollClicked = {},
)
}

View file

@ -35,6 +35,7 @@ sealed interface MessageComposerEvents {
data object PhotoFromCamera : PickAttachmentSource
data object VideoFromCamera : PickAttachmentSource
data object Location : PickAttachmentSource
data object Poll : PickAttachmentSource
}
data object CancelSendAttachment : MessageComposerEvents
}

View file

@ -83,6 +83,11 @@ class MessageComposerPresenter @Inject constructor(
canShareLocation.value = featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing)
}
val canCreatePoll = remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
canCreatePoll.value = featureFlagService.isFeatureEnabled(FeatureFlags.Polls)
}
val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker { uri, mimeType ->
handlePickedMedia(attachmentsState, uri, mimeType)
}
@ -179,6 +184,10 @@ class MessageComposerPresenter @Inject constructor(
showAttachmentSourcePicker = false
// Navigation to the location picker screen is done at the view layer
}
MessageComposerEvents.PickAttachmentSource.Poll -> {
showAttachmentSourcePicker = false
// Navigation to the create poll screen is done at the view layer
}
is MessageComposerEvents.CancelSendAttachment -> {
ongoingSendAttachmentJob.value?.let {
it.cancel()
@ -195,6 +204,7 @@ class MessageComposerPresenter @Inject constructor(
mode = messageComposerContext.composerMode,
showAttachmentSourcePicker = showAttachmentSourcePicker,
canShareLocation = canShareLocation.value,
canCreatePoll = canCreatePoll.value,
attachmentsState = attachmentsState.value,
eventSink = ::handleEvents
)

View file

@ -29,6 +29,7 @@ data class MessageComposerState(
val mode: MessageComposerMode,
val showAttachmentSourcePicker: Boolean,
val canShareLocation: Boolean,
val canCreatePoll: Boolean,
val attachmentsState: AttachmentsState,
val eventSink: (MessageComposerEvents) -> Unit
) {

View file

@ -33,6 +33,7 @@ fun aMessageComposerState(
mode: MessageComposerMode = MessageComposerMode.Normal(content = ""),
showAttachmentSourcePicker: Boolean = false,
canShareLocation: Boolean = true,
canCreatePoll: Boolean = true,
attachmentsState: AttachmentsState = AttachmentsState.None,
) = MessageComposerState(
text = text,
@ -41,6 +42,7 @@ fun aMessageComposerState(
mode = mode,
showAttachmentSourcePicker = showAttachmentSourcePicker,
canShareLocation = canShareLocation,
canCreatePoll = canCreatePoll,
attachmentsState = attachmentsState,
eventSink = {},
)

View file

@ -29,6 +29,7 @@ import io.element.android.libraries.textcomposer.TextComposer
fun MessageComposerView(
state: MessageComposerState,
onSendLocationClicked: () -> Unit,
onCreatePollClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
fun onFullscreenToggle() {
@ -59,6 +60,7 @@ fun MessageComposerView(
AttachmentsBottomSheet(
state = state,
onSendLocationClicked = onSendLocationClicked,
onCreatePollClicked = onCreatePollClicked,
)
TextComposer(
@ -88,6 +90,7 @@ internal fun MessageComposerViewDarkPreview(@PreviewParameter(MessageComposerSta
private fun ContentToPreview(state: MessageComposerState) {
MessageComposerView(
state = state,
onSendLocationClicked = {}
onSendLocationClicked = {},
onCreatePollClicked = {},
)
}

View file

@ -21,7 +21,9 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.LocalContentColor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
@ -35,6 +37,7 @@ import io.element.android.libraries.designsystem.components.ClickableLinkText
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.text.toAnnotatedString
import io.element.android.libraries.theme.ElementTheme
@Composable
fun TimelineItemTextView(
@ -45,31 +48,33 @@ fun TimelineItemTextView(
onTextClicked: () -> Unit = {},
onTextLongClicked: () -> Unit = {},
) {
val htmlDocument = content.htmlDocument
if (htmlDocument != null) {
// For now we ignore the extra padding for html content, so add some spacing
// below the content (as previous behavior)
Column(modifier = modifier) {
HtmlDocument(
document = htmlDocument,
modifier = Modifier,
onTextClicked = onTextClicked,
onTextLongClicked = onTextLongClicked,
interactionSource = interactionSource
)
Spacer(Modifier.height(16.dp))
}
} else {
Box(modifier) {
val textWithPadding = remember(content.body) {
content.body + extraPadding.getStr(16.sp).toAnnotatedString()
CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textPrimary) {
val htmlDocument = content.htmlDocument
if (htmlDocument != null) {
// For now we ignore the extra padding for html content, so add some spacing
// below the content (as previous behavior)
Column(modifier = modifier) {
HtmlDocument(
document = htmlDocument,
modifier = Modifier,
onTextClicked = onTextClicked,
onTextLongClicked = onTextLongClicked,
interactionSource = interactionSource
)
Spacer(Modifier.height(16.dp))
}
} else {
Box(modifier) {
val textWithPadding = remember(content.body) {
content.body + extraPadding.getStr(16.sp).toAnnotatedString()
}
ClickableLinkText(
text = textWithPadding,
onClick = onTextClicked,
onLongClick = onTextLongClicked,
interactionSource = interactionSource
)
}
ClickableLinkText(
text = textWithPadding,
onClick = onTextClicked,
onLongClick = onTextLongClicked,
interactionSource = interactionSource
)
}
}
}

View file

@ -69,7 +69,6 @@ class TimelineItemContentPollFactory @Inject constructor(
return TimelineItemPollContent(
question = content.question,
answerItems = answerItems,
votes = content.votes,
pollKind = content.kind,
isEnded = isEndedPoll,
)

View file

@ -17,13 +17,11 @@
package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.features.poll.api.PollAnswerItem
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.poll.PollKind
data class TimelineItemPollContent(
val question: String,
val answerItems: List<PollAnswerItem>,
val votes: Map<String, List<UserId>>,
val pollKind: PollKind,
val isEnded: Boolean,
) : TimelineItemEventContent {

View file

@ -34,6 +34,5 @@ fun aTimelineItemPollContent(): TimelineItemPollContent {
question = "What type of food should we have at the party?",
answerItems = aPollAnswerItemList(),
isEnded = false,
votes = emptyMap(),
)
}

View file

@ -26,10 +26,14 @@ 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
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
import io.element.android.features.poll.api.PollAnswerItem
import io.element.android.libraries.matrix.api.poll.PollAnswer
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.core.aBuildMeta
import kotlinx.collections.immutable.persistentListOf
@ -369,6 +373,56 @@ class ActionListPresenterTest {
assertThat(successState.displayEmojiReactions).isFalse()
}
}
@Test
fun `present - compute for poll message`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = false)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = true,
content = TimelineItemPollContent(
question = "Some question?",
answerItems = listOf(
PollAnswerItem(
answer = PollAnswer("id_1", "Answer1"),
isSelected = false,
isEnabled = false,
isWinner = false,
isDisclosed = false,
votesCount = 0,
percentage = 0.0f,
),
PollAnswerItem(
answer = PollAnswer("id_2", "Answer2"),
isSelected = false,
isEnabled = false,
isWinner = false,
isDisclosed = false,
votesCount = 0,
percentage = 0.0f,
),
),
pollKind = PollKind.Disclosed,
isEnded = false,
)
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
messageEvent,
persistentListOf(
TimelineItemAction.Redact,
)
)
)
assertThat(successState.displayEmojiReactions).isTrue()
}
}
}
private fun anActionListPresenter(isBuildDebuggable: Boolean) = ActionListPresenter(buildMeta = aBuildMeta(isDebuggable = isBuildDebuggable))

View file

@ -0,0 +1,279 @@
/*
* 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.messages.timeline.factories.event
import com.google.common.truth.Truth
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentPollFactory
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
import io.element.android.features.poll.api.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.UserId
import io.element.android.libraries.matrix.api.poll.PollAnswer
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_10
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.A_USER_ID_3
import io.element.android.libraries.matrix.test.A_USER_ID_4
import io.element.android.libraries.matrix.test.A_USER_ID_5
import io.element.android.libraries.matrix.test.A_USER_ID_6
import io.element.android.libraries.matrix.test.A_USER_ID_7
import io.element.android.libraries.matrix.test.A_USER_ID_8
import io.element.android.libraries.matrix.test.A_USER_ID_9
import io.element.android.libraries.matrix.test.FakeMatrixClient
import kotlinx.coroutines.test.runTest
import org.junit.Test
internal class TimelineItemContentPollFactoryTest {
private val factory = TimelineItemContentPollFactory(
matrixClient = FakeMatrixClient(),
featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.Polls.key to true)),
)
@Test
fun `Disclosed poll - not ended, no votes`() = runTest {
Truth.assertThat(factory.create(aPollContent())).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 }
Truth.assertThat(
factory.create(aPollContent(votes = votes))
)
.isEqualTo(
aTimelineItemPollContent(
answerItems = listOf(
aPollAnswerItem(answer = A_POLL_ANSWER_1, votesCount = 3, percentage = 0.3f),
aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, votesCount = 6, percentage = 0.6f),
aPollAnswerItem(answer = A_POLL_ANSWER_3),
aPollAnswerItem(answer = A_POLL_ANSWER_4, votesCount = 1, percentage = 0.1f),
),
)
)
}
@Test
fun `Disclosed poll - ended, no votes, no winner`() = runTest {
Truth.assertThat(
factory.create(aPollContent(endTime = 1UL))
).isEqualTo(
aTimelineItemPollContent().let {
it.copy(
answerItems = it.answerItems.map { answerItem -> answerItem.copy(isEnabled = false) },
isEnded = true,
)
}
)
}
@Test
fun `Disclosed poll - ended, some votes, including one from current user (winner)`() = runTest {
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }
Truth.assertThat(
factory.create(aPollContent(votes = votes, endTime = 1UL))
)
.isEqualTo(
aTimelineItemPollContent(
answerItems = listOf(
aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, votesCount = 3, percentage = 0.3f),
aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, isWinner = true, votesCount = 6, percentage = 0.6f),
aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false),
aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, votesCount = 1, percentage = 0.1f),
),
isEnded = true,
)
)
}
@Test
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 }
Truth.assertThat(
factory.create(aPollContent(votes = votes, endTime = 1UL))
)
.isEqualTo(
aTimelineItemPollContent(
answerItems = listOf(
aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f),
aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, votesCount = 2, percentage = 0.2f),
aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false),
aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f),
),
isEnded = true,
)
)
}
@Test
fun `Undisclosed poll - not ended, no votes`() = runTest {
Truth.assertThat(
factory.create(aPollContent(PollKind.Undisclosed).copy())
).isEqualTo(
aTimelineItemPollContent(PollKind.Undisclosed).let {
it.copy(answerItems = it.answerItems.map { answerItem -> answerItem.copy(isDisclosed = false) })
}
)
}
@Test
fun `Undisclosed poll - not ended, some votes, including one from current user`() = runTest {
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }
Truth.assertThat(
factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes))
)
.isEqualTo(
aTimelineItemPollContent(
pollKind = PollKind.Undisclosed,
answerItems = listOf(
aPollAnswerItem(answer = A_POLL_ANSWER_1, isDisclosed = false, votesCount = 3, percentage = 0.3f),
aPollAnswerItem(answer = A_POLL_ANSWER_2, isDisclosed = false, isSelected = true, votesCount = 6, percentage = 0.6f),
aPollAnswerItem(answer = A_POLL_ANSWER_3, isDisclosed = false),
aPollAnswerItem(answer = A_POLL_ANSWER_4, isDisclosed = false, votesCount = 1, percentage = 0.1f),
),
)
)
}
@Test
fun `Undisclosed poll - ended, no votes, no winner`() = runTest {
Truth.assertThat(
factory.create(aPollContent(pollKind = PollKind.Undisclosed, endTime = 1UL))
).isEqualTo(
aTimelineItemPollContent().let {
it.copy(
pollKind = PollKind.Undisclosed,
answerItems = it.answerItems.map { answerItem ->
answerItem.copy(isDisclosed = true, isEnabled = false, isWinner = false)
},
isEnded = true,
)
}
)
}
@Test
fun `Undisclosed poll - ended, some votes, including one from current user (winner)`() = runTest {
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }
Truth.assertThat(
factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes, endTime = 1UL))
)
.isEqualTo(
aTimelineItemPollContent(
pollKind = PollKind.Undisclosed,
answerItems = listOf(
aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, votesCount = 3, percentage = 0.3f),
aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, isWinner = true, votesCount = 6, percentage = 0.6f),
aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false),
aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, votesCount = 1, percentage = 0.1f),
),
isEnded = true,
)
)
}
@Test
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 }
Truth.assertThat(
factory.create(aPollContent(PollKind.Undisclosed).copy(votes = votes, endTime = 1UL))
)
.isEqualTo(
aTimelineItemPollContent(
pollKind = PollKind.Undisclosed,
answerItems = listOf(
aPollAnswerItem(A_POLL_ANSWER_1, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f),
aPollAnswerItem(A_POLL_ANSWER_2, isSelected = true, isEnabled = false, votesCount = 2, percentage = 0.2f),
aPollAnswerItem(A_POLL_ANSWER_3, isEnabled = false),
aPollAnswerItem(A_POLL_ANSWER_4, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f),
),
isEnded = true,
)
)
}
private fun aPollContent(
pollKind: PollKind = PollKind.Disclosed,
votes: Map<String, List<UserId>> = emptyMap(),
endTime: ULong? = null,
): PollContent = PollContent(
question = A_POLL_QUESTION,
kind = pollKind,
maxSelections = 1UL,
answers = listOf(A_POLL_ANSWER_1, A_POLL_ANSWER_2, A_POLL_ANSWER_3, A_POLL_ANSWER_4),
votes = votes,
endTime = endTime,
)
private fun aTimelineItemPollContent(
pollKind: PollKind = PollKind.Disclosed,
answerItems: List<PollAnswerItem> = listOf(
aPollAnswerItem(A_POLL_ANSWER_1),
aPollAnswerItem(A_POLL_ANSWER_2),
aPollAnswerItem(A_POLL_ANSWER_3),
aPollAnswerItem(A_POLL_ANSWER_4),
),
isEnded: Boolean = false,
) = TimelineItemPollContent(
question = A_POLL_QUESTION,
answerItems = answerItems,
pollKind = pollKind,
isEnded = isEnded,
)
private fun aPollAnswerItem(
answer: PollAnswer,
isSelected: Boolean = false,
isEnabled: Boolean = true,
isWinner: Boolean = false,
isDisclosed: Boolean = true,
votesCount: Int = 0,
percentage: Float = 0f,
) = PollAnswerItem(
answer = answer,
isSelected = isSelected,
isEnabled = isEnabled,
isWinner = isWinner,
isDisclosed = isDisclosed,
votesCount = votesCount,
percentage = percentage,
)
private companion object TestData {
private const val A_POLL_QUESTION = "What is your favorite food?"
private val A_POLL_ANSWER_1 = PollAnswer("id_1", "Pizza")
private val A_POLL_ANSWER_2 = PollAnswer("id_2", "Pasta")
private val A_POLL_ANSWER_3 = PollAnswer("id_3", "French Fries")
private val A_POLL_ANSWER_4 = PollAnswer("id_4", "Hamburger")
private val MY_USER_WINNING_VOTES = mapOf(
A_POLL_ANSWER_1 to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4),
A_POLL_ANSWER_2 to listOf(A_USER_ID /* my vote */, A_USER_ID_5, A_USER_ID_6, A_USER_ID_7, A_USER_ID_8, A_USER_ID_9), // winner
A_POLL_ANSWER_3 to emptyList(),
A_POLL_ANSWER_4 to listOf(A_USER_ID_10),
)
private val OTHER_WINNING_VOTES = mapOf(
A_POLL_ANSWER_1 to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4, A_USER_ID_5), // winner
A_POLL_ANSWER_2 to listOf(A_USER_ID /* my vote */, A_USER_ID_6),
A_POLL_ANSWER_3 to emptyList(),
A_POLL_ANSWER_4 to listOf(A_USER_ID_7, A_USER_ID_8, A_USER_ID_9, A_USER_ID_10), // winner
)
}
}