Add feature flag for polls (#1064)

* Handle poll events from the sdk

* Render started poll event in the timeline

* Create poll module

* Check poll kind before revealing the results

* Check if user has voted before revealing the results

* Add active poll previews

* Minor cleanup

* Update todos

* Fix CI

* Remove hardcoded string

* Update preview

* changelog file

* Update screenshots

* Use CommonPlurals

* Set poll root view as selectableGroup

* Improve poll result rendering

* Update screenshots

* Add missing showkase processor

* Update screenshots

* Add feature flag for polls

* Add supporting text in PreferenceCheckbox

* Render poll events if feature flag is enabled

* changelog

* Update screenshots

* Fix tests

* Move feature flag check to poll factory

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Florian Renaud 2023-08-16 17:14:38 +02:00 committed by GitHub
parent 9cb2424101
commit 1083b431b6
18 changed files with 72 additions and 31 deletions

1
changelog.d/1064.wip Normal file
View file

@ -0,0 +1 @@
[Poll] Add feature flag in developer options

View file

@ -89,7 +89,7 @@ class TimelineItemsFactory @Inject constructor(
this.timelineItems.emit(result) this.timelineItems.emit(result)
} }
private fun buildAndCacheItem( private suspend fun buildAndCacheItem(
timelineItems: List<MatrixTimelineItem>, timelineItems: List<MatrixTimelineItem>,
index: Int index: Int
): TimelineItem? { ): TimelineItem? {

View file

@ -47,7 +47,7 @@ class TimelineItemContentFactory @Inject constructor(
private val failedToParseStateFactory: TimelineItemContentFailedToParseStateFactory private val failedToParseStateFactory: TimelineItemContentFailedToParseStateFactory
) { ) {
fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent { suspend fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent {
return when (val itemContent = eventTimelineItem.content) { return when (val itemContent = eventTimelineItem.content) {
is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent) is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent)
is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent) is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent)

View file

@ -18,7 +18,10 @@ 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.poll.api.PollAnswerItem import io.element.android.features.poll.api.PollAnswerItem
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.MatrixClient
import io.element.android.libraries.matrix.api.poll.PollKind 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.api.timeline.item.event.PollContent
@ -26,9 +29,12 @@ import javax.inject.Inject
class TimelineItemContentPollFactory @Inject constructor( class TimelineItemContentPollFactory @Inject constructor(
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val featureFlagService: FeatureFlagService,
) { ) {
fun create(content: PollContent): TimelineItemEventContent { suspend fun create(content: PollContent): TimelineItemEventContent {
if (!featureFlagService.isFeatureEnabled(FeatureFlags.Polls)) return TimelineItemUnknownContent
// Todo Move this computation to the matrix rust sdk // Todo Move this computation to the matrix rust sdk
val showResults = content.kind == PollKind.Disclosed && matrixClient.sessionId in content.votes.flatMap { it.value } val showResults = content.kind == PollKind.Disclosed && matrixClient.sessionId in content.votes.flatMap { it.value }
val pollVotesCount = content.votes.flatMap { it.value }.size val pollVotesCount = content.votes.flatMap { it.value }.size

View file

@ -38,7 +38,7 @@ class TimelineItemEventFactory @Inject constructor(
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
) { ) {
fun create( suspend fun create(
currentTimelineItem: MatrixTimelineItem.Event, currentTimelineItem: MatrixTimelineItem.Event,
index: Int, index: Int,
timelineItems: List<MatrixTimelineItem>, timelineItems: List<MatrixTimelineItem>,

View file

@ -37,6 +37,7 @@ import io.element.android.features.messages.impl.timeline.util.FileExtensionExtr
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
@ -52,14 +53,14 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
messageFactory = TimelineItemContentMessageFactory(FakeFileSizeFormatter(), FileExtensionExtractorWithoutValidation()), messageFactory = TimelineItemContentMessageFactory(FakeFileSizeFormatter(), FileExtensionExtractorWithoutValidation()),
redactedMessageFactory = TimelineItemContentRedactedFactory(), redactedMessageFactory = TimelineItemContentRedactedFactory(),
stickerFactory = TimelineItemContentStickerFactory(), stickerFactory = TimelineItemContentStickerFactory(),
pollFactory = TimelineItemContentPollFactory(matrixClient), pollFactory = TimelineItemContentPollFactory(matrixClient, FakeFeatureFlagService()),
pollEndFactory = TimelineItemContentPollEndFactory(), pollEndFactory = TimelineItemContentPollEndFactory(),
utdFactory = TimelineItemContentUTDFactory(), utdFactory = TimelineItemContentUTDFactory(),
roomMembershipFactory = TimelineItemContentRoomMembershipFactory(timelineEventFormatter), roomMembershipFactory = TimelineItemContentRoomMembershipFactory(timelineEventFormatter),
profileChangeFactory = TimelineItemContentProfileChangeFactory(timelineEventFormatter), profileChangeFactory = TimelineItemContentProfileChangeFactory(timelineEventFormatter),
stateFactory = TimelineItemContentStateFactory(timelineEventFormatter), stateFactory = TimelineItemContentStateFactory(timelineEventFormatter),
failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(), failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(),
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory() failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory(),
), ),
matrixClient = matrixClient, matrixClient = matrixClient,
), ),

View file

@ -110,6 +110,7 @@ class DeveloperSettingsPresenter @Inject constructor(
FeatureUiModel( FeatureUiModel(
key = feature.key, key = feature.key,
title = feature.title, title = feature.title,
description = feature.description,
isEnabled = isEnabled isEnabled = isEnabled
) )
} }

View file

@ -17,6 +17,8 @@
package io.element.android.libraries.designsystem.components.preferences package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -35,6 +37,7 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.Checkbox import io.element.android.libraries.designsystem.theme.components.Checkbox
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.toEnabledColor import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.designsystem.toSecondaryEnabledColor
import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.theme.ElementTheme
@Composable @Composable
@ -42,6 +45,7 @@ fun PreferenceCheckbox(
title: String, title: String,
isChecked: Boolean, isChecked: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
supportingText: String? = null,
enabled: Boolean = true, enabled: Boolean = true,
icon: ImageVector? = null, icon: ImageVector? = null,
showIconAreaIfNoIcon: Boolean = false, showIconAreaIfNoIcon: Boolean = false,
@ -60,13 +64,23 @@ fun PreferenceCheckbox(
enabled = enabled, enabled = enabled,
isVisible = showIconAreaIfNoIcon isVisible = showIconAreaIfNoIcon
) )
Text( Column(
modifier = Modifier modifier = Modifier.weight(1f),
.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp),
style = ElementTheme.typography.fontBodyLgRegular, ) {
text = title, Text(
color = enabled.toEnabledColor(), style = ElementTheme.typography.fontBodyLgRegular,
) text = title,
color = enabled.toEnabledColor(),
)
if (supportingText != null) {
Text(
style = ElementTheme.typography.fontBodyMdRegular,
text = supportingText,
color = enabled.toSecondaryEnabledColor(),
)
}
}
Checkbox( Checkbox(
modifier = Modifier modifier = Modifier
.align(Alignment.CenterVertically), .align(Alignment.CenterVertically),
@ -83,10 +97,19 @@ internal fun PreferenceCheckboxPreview() = ElementThemedPreview { ContentToPrevi
@Composable @Composable
private fun ContentToPreview() { private fun ContentToPreview() {
PreferenceCheckbox( Column {
title = "Checkbox", PreferenceCheckbox(
icon = Icons.Default.Announcement, title = "Checkbox",
enabled = true, icon = Icons.Default.Announcement,
isChecked = true enabled = true,
) isChecked = true
)
PreferenceCheckbox(
title = "Checkbox with supporting text",
supportingText = "Supporting text",
icon = Icons.Default.Announcement,
enabled = true,
isChecked = true
)
}
} }

View file

@ -25,5 +25,11 @@ enum class FeatureFlags(
LocationSharing( LocationSharing(
key = "feature.locationsharing", key = "feature.locationsharing",
title = "Allow user to share location", title = "Allow user to share location",
),
Polls(
key = "feature.polls",
title = "Polls",
description = "Render poll events in the timeline",
defaultValue = false,
) )
} }

View file

@ -30,6 +30,7 @@ class BuildtimeFeatureFlagProvider @Inject constructor() :
return if (feature is FeatureFlags) { return if (feature is FeatureFlags) {
when (feature) { when (feature) {
FeatureFlags.LocationSharing -> true FeatureFlags.LocationSharing -> true
FeatureFlags.Polls -> false
} }
} else { } else {
false false

View file

@ -54,6 +54,7 @@ fun FeaturePreferenceView(
) { ) {
PreferenceCheckbox( PreferenceCheckbox(
title = feature.title, title = feature.title,
supportingText = feature.description,
isChecked = feature.isEnabled, isChecked = feature.isEnabled,
modifier = modifier, modifier = modifier,
onCheckedChange = onCheckedChange onCheckedChange = onCheckedChange

View file

@ -19,5 +19,6 @@ package io.element.android.libraries.featureflag.ui.model
data class FeatureUiModel( data class FeatureUiModel(
val key: String, val key: String,
val title: String, val title: String,
val description: String?,
val isEnabled: Boolean val isEnabled: Boolean
) )

View file

@ -21,7 +21,7 @@ import kotlinx.collections.immutable.persistentListOf
fun aFeatureUiModelList(): ImmutableList<FeatureUiModel> { fun aFeatureUiModelList(): ImmutableList<FeatureUiModel> {
return persistentListOf( return persistentListOf(
FeatureUiModel("key1", "Display State Events", true), FeatureUiModel("key1", "Display State Events", "Show state events in the timeline", true),
FeatureUiModel("key2", "Display Room Events", false) FeatureUiModel("key2", "Display Room Events", null, false),
) )
} }

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4a9a3902dd33ab3e528fa9c534302228113cc468914025766a21397ad3eda9e2 oid sha256:aa65609944cd9118668fcdf75a7882111230cceebbdc68bab764f51929dfc6f2
size 45064 size 49584

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4a9a3902dd33ab3e528fa9c534302228113cc468914025766a21397ad3eda9e2 oid sha256:aa65609944cd9118668fcdf75a7882111230cceebbdc68bab764f51929dfc6f2
size 45064 size 49584

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:cd1cc710443af62efddf0ea165a6289912f471f669803255368d3266b872f5f6 oid sha256:c571049a9cbf3dc13d8a700aa4d3639f8fca89db749c5ebfbb59ca6a0bb0b9eb
size 49829 size 54206

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:cd1cc710443af62efddf0ea165a6289912f471f669803255368d3266b872f5f6 oid sha256:c571049a9cbf3dc13d8a700aa4d3639f8fca89db749c5ebfbb59ca6a0bb0b9eb
size 49829 size 54206

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:06813451633c48fccac6f70810ecb9465946b643376fccd61312b6064b0a3a40 oid sha256:7ba8a794a8b65f412806ee1accc4f8678b046e880fd76c038eb054155ba514ea
size 11518 size 28463