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:
parent
9cb2424101
commit
1083b431b6
18 changed files with 72 additions and 31 deletions
1
changelog.d/1064.wip
Normal file
1
changelog.d/1064.wip
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
[Poll] Add feature flag in developer options
|
||||||
|
|
@ -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? {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue