"View only" polls in the timeline (#1031)
* 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 --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
1
changelog.d/1031.wip
Normal file
1
changelog.d/1031.wip
Normal file
@@ -0,0 +1 @@
|
||||
[Poll] Render start event in the timeline
|
||||
@@ -34,6 +34,7 @@ dependencies {
|
||||
anvil(projects.anvilcodegen)
|
||||
api(projects.features.messages.api)
|
||||
implementation(projects.features.location.api)
|
||||
implementation(projects.features.poll.api)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
|
||||
@@ -47,6 +47,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
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.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.TimelineItemTextBasedContent
|
||||
@@ -277,6 +278,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||
is TimelineItemLocationContent -> AttachmentThumbnailInfo(
|
||||
type = AttachmentThumbnailType.Location,
|
||||
)
|
||||
is TimelineItemPollContent, // TODO Polls: handle reply to
|
||||
is TimelineItemTextBasedContent,
|
||||
is TimelineItemRedactedContent,
|
||||
is TimelineItemStateContent,
|
||||
|
||||
@@ -62,6 +62,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
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.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.TimelineItemTextBasedContent
|
||||
@@ -236,6 +237,7 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
|
||||
val textContent = remember(event.content) { formatter.format(event) }
|
||||
|
||||
when (event.content) {
|
||||
is TimelineItemPollContent, // TODO Polls: handle summary
|
||||
is TimelineItemTextBasedContent,
|
||||
is TimelineItemStateContent,
|
||||
is TimelineItemEncryptedContent,
|
||||
|
||||
@@ -25,6 +25,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
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.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.TimelineItemTextBasedContent
|
||||
@@ -90,5 +91,10 @@ fun TimelineItemEventContentView(
|
||||
content = content,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemPollContent -> TimelineItemPollView(
|
||||
content = content,
|
||||
onAnswerSelected = {},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.impl.timeline.components.event
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
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.ActivePollContentView
|
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun TimelineItemPollView(
|
||||
content: TimelineItemPollContent,
|
||||
onAnswerSelected: (PollAnswer) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ActivePollContentView(
|
||||
question = content.question,
|
||||
answerItems = content.answerItems.toImmutableList(),
|
||||
pollKind = content.pollKind,
|
||||
onAnswerSelected = onAnswerSelected,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
internal fun TimelineItemPollViewPreview(@PreviewParameter(TimelineItemPollContentProvider::class) content: TimelineItemPollContent) =
|
||||
ElementPreview {
|
||||
TimelineItemPollView(
|
||||
content = content,
|
||||
onAnswerSelected = {},
|
||||
)
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventTimeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollEndContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
@@ -35,6 +37,8 @@ class TimelineItemContentFactory @Inject constructor(
|
||||
private val messageFactory: TimelineItemContentMessageFactory,
|
||||
private val redactedMessageFactory: TimelineItemContentRedactedFactory,
|
||||
private val stickerFactory: TimelineItemContentStickerFactory,
|
||||
private val pollFactory: TimelineItemContentPollFactory,
|
||||
private val pollEndFactory: TimelineItemContentPollEndFactory,
|
||||
private val utdFactory: TimelineItemContentUTDFactory,
|
||||
private val roomMembershipFactory: TimelineItemContentRoomMembershipFactory,
|
||||
private val profileChangeFactory: TimelineItemContentProfileChangeFactory,
|
||||
@@ -53,6 +57,8 @@ class TimelineItemContentFactory @Inject constructor(
|
||||
is RoomMembershipContent -> roomMembershipFactory.create(eventTimelineItem)
|
||||
is StateContent -> stateFactory.create(eventTimelineItem)
|
||||
is StickerContent -> stickerFactory.create(itemContent)
|
||||
is PollContent -> pollFactory.create(itemContent)
|
||||
is PollEndContent -> pollEndFactory.create(itemContent)
|
||||
is UnableToDecryptContent -> utdFactory.create(itemContent)
|
||||
is UnknownContent -> TimelineItemUnknownContent
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.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.TimelineItemUnknownContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollEndContent
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemContentPollEndFactory @Inject constructor() {
|
||||
|
||||
fun create(@Suppress("UNUSED_PARAMETER") content: PollEndContent): TimelineItemEventContent {
|
||||
return TimelineItemUnknownContent
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.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.poll.api.PollAnswerItem
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemContentPollFactory @Inject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
) {
|
||||
|
||||
fun create(content: PollContent): TimelineItemEventContent {
|
||||
// Todo Move this computation to the matrix rust sdk
|
||||
val showResults = content.kind == PollKind.Disclosed && matrixClient.sessionId in content.votes.flatMap { it.value }
|
||||
val pollVotesCount = content.votes.flatMap { it.value }.size
|
||||
val userVotes = content.votes.filter { matrixClient.sessionId in it.value }.keys
|
||||
val answerItems = content.answers.map { answer ->
|
||||
val votesCount = content.votes[answer.id]?.size ?: 0
|
||||
val progress = if (pollVotesCount > 0) votesCount.toFloat() / pollVotesCount.toFloat() else 0f
|
||||
PollAnswerItem(
|
||||
answer = answer,
|
||||
isSelected = answer.id in userVotes,
|
||||
isDisclosed = showResults,
|
||||
votesCount = votesCount,
|
||||
progress = progress,
|
||||
)
|
||||
}
|
||||
|
||||
return TimelineItemPollContent(
|
||||
question = content.question,
|
||||
answerItems = answerItems,
|
||||
votes = content.votes,
|
||||
pollKind = content.kind,
|
||||
isDisclosed = showResults
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
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.TimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRoomMembershipContent
|
||||
@@ -33,6 +34,8 @@ import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollEndContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
@@ -55,6 +58,7 @@ internal fun TimelineItem.Event.canBeGrouped(): Boolean {
|
||||
is TimelineItemVideoContent,
|
||||
is TimelineItemAudioContent,
|
||||
is TimelineItemLocationContent,
|
||||
is TimelineItemPollContent,
|
||||
TimelineItemRedactedContent,
|
||||
TimelineItemUnknownContent -> false
|
||||
is TimelineItemProfileChangeContent,
|
||||
@@ -74,6 +78,8 @@ internal fun MatrixTimelineItem.Event.canBeDisplayedInBubbleBlock(): Boolean {
|
||||
is MessageContent,
|
||||
RedactedContent,
|
||||
is StickerContent,
|
||||
is PollContent,
|
||||
is PollEndContent,
|
||||
is UnableToDecryptContent -> true
|
||||
is FailedToParseStateContent,
|
||||
is ProfileChangeContent,
|
||||
|
||||
@@ -45,6 +45,7 @@ fun TimelineItemEventContent.canReact(): Boolean =
|
||||
is TimelineItemFileContent,
|
||||
is TimelineItemImageContent,
|
||||
is TimelineItemLocationContent,
|
||||
is TimelineItemPollContent,
|
||||
is TimelineItemVideoContent -> true
|
||||
is TimelineItemStateContent,
|
||||
is TimelineItemRedactedContent,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2022 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.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 isDisclosed: Boolean,
|
||||
) : TimelineItemEventContent {
|
||||
override val type: String = "TimelineItemPollContent"
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.impl.timeline.model.event
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.poll.api.aPollAnswerItemList
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
|
||||
open class TimelineItemPollContentProvider : PreviewParameterProvider<TimelineItemPollContent> {
|
||||
override val values: Sequence<TimelineItemPollContent>
|
||||
get() = sequenceOf(
|
||||
aTimelineItemPollContent(),
|
||||
aTimelineItemPollContent().copy(isDisclosed = true),
|
||||
)
|
||||
}
|
||||
|
||||
fun aTimelineItemPollContent(): TimelineItemPollContent {
|
||||
return TimelineItemPollContent(
|
||||
pollKind = PollKind.Disclosed,
|
||||
isDisclosed = false,
|
||||
question = "What type of food should we have at the party?",
|
||||
answerItems = aPollAnswerItemList(),
|
||||
votes = emptyMap(),
|
||||
)
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
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.TimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
@@ -47,6 +48,7 @@ class MessageSummaryFormatterImpl @Inject constructor(
|
||||
is TimelineItemLocationContent -> context.getString(CommonStrings.common_shared_location)
|
||||
is TimelineItemEncryptedContent -> context.getString(CommonStrings.common_unable_to_decrypt)
|
||||
is TimelineItemRedactedContent -> context.getString(CommonStrings.common_message_removed)
|
||||
is TimelineItemPollContent, // Todo Polls: handle summary
|
||||
is TimelineItemUnknownContent -> context.getString(CommonStrings.common_unsupported_event)
|
||||
is TimelineItemImageContent -> context.getString(CommonStrings.common_image)
|
||||
is TimelineItemVideoContent -> context.getString(CommonStrings.common_video)
|
||||
|
||||
@@ -21,6 +21,8 @@ import io.element.android.features.messages.impl.timeline.factories.event.Timeli
|
||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFailedToParseMessageFactory
|
||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFailedToParseStateFactory
|
||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentMessageFactory
|
||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentPollEndFactory
|
||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentPollFactory
|
||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentProfileChangeFactory
|
||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentRedactedFactory
|
||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentRoomMembershipFactory
|
||||
@@ -42,6 +44,7 @@ import kotlinx.coroutines.test.TestScope
|
||||
|
||||
internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
|
||||
val timelineEventFormatter = aTimelineEventFormatter()
|
||||
val matrixClient = FakeMatrixClient()
|
||||
return TimelineItemsFactory(
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
eventItemFactory = TimelineItemEventFactory(
|
||||
@@ -49,6 +52,8 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
|
||||
messageFactory = TimelineItemContentMessageFactory(FakeFileSizeFormatter(), FileExtensionExtractorWithoutValidation()),
|
||||
redactedMessageFactory = TimelineItemContentRedactedFactory(),
|
||||
stickerFactory = TimelineItemContentStickerFactory(),
|
||||
pollFactory = TimelineItemContentPollFactory(matrixClient),
|
||||
pollEndFactory = TimelineItemContentPollEndFactory(),
|
||||
utdFactory = TimelineItemContentUTDFactory(),
|
||||
roomMembershipFactory = TimelineItemContentRoomMembershipFactory(timelineEventFormatter),
|
||||
profileChangeFactory = TimelineItemContentProfileChangeFactory(timelineEventFormatter),
|
||||
@@ -56,7 +61,7 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
|
||||
failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(),
|
||||
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory()
|
||||
),
|
||||
matrixClient = FakeMatrixClient(),
|
||||
matrixClient = matrixClient,
|
||||
),
|
||||
virtualItemFactory = TimelineItemVirtualFactory(
|
||||
daySeparatorFactory = TimelineItemDaySeparatorFactory(
|
||||
|
||||
35
features/poll/api/build.gradle.kts
Normal file
35
features/poll/api/build.gradle.kts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.poll.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.androidx.constraintlayout.compose)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BarChart
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun ActivePollContentView(
|
||||
question: String,
|
||||
answerItems: ImmutableList<PollAnswerItem>,
|
||||
pollKind: PollKind,
|
||||
onAnswerSelected: (PollAnswer) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val showResults = answerItems.any { it.isSelected }
|
||||
Column(
|
||||
modifier = modifier
|
||||
.selectableGroup()
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.BarChart, contentDescription = null)
|
||||
Text(
|
||||
text = question,
|
||||
style = ElementTheme.typography.fontBodyLgMedium
|
||||
)
|
||||
}
|
||||
|
||||
answerItems.forEach { answerItem ->
|
||||
PollAnswerView(
|
||||
answerItem = answerItem,
|
||||
onClick = { onAnswerSelected(answerItem.answer) }
|
||||
)
|
||||
}
|
||||
|
||||
val votesCount = answerItems.sumOf { it.votesCount }
|
||||
when {
|
||||
pollKind == PollKind.Undisclosed -> {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Start)
|
||||
.padding(start = 32.dp),
|
||||
style = ElementTheme.typography.fontBodyXsRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
text = stringResource(CommonStrings.common_poll_undisclosed_text),
|
||||
)
|
||||
}
|
||||
showResults -> {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
style = ElementTheme.typography.fontBodyXsRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
text = stringResource(CommonStrings.common_poll_total_votes, votesCount),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
internal fun ActivePollContentNoResultsPreview() = ElementPreview {
|
||||
ActivePollContentView(
|
||||
question = "What type of food should we have at the party?",
|
||||
answerItems = aPollAnswerItemList(isDisclosed = false),
|
||||
pollKind = PollKind.Undisclosed,
|
||||
onAnswerSelected = { },
|
||||
)
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
internal fun ActivePollContentWithResultsPreview() = ElementPreview {
|
||||
ActivePollContentView(
|
||||
question = "What type of food should we have at the party?",
|
||||
answerItems = aPollAnswerItemList(),
|
||||
pollKind = PollKind.Disclosed,
|
||||
onAnswerSelected = { },
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||
|
||||
/**
|
||||
* UI model for a [PollAnswer].
|
||||
*
|
||||
* @property answer the poll answer.
|
||||
* @property isSelected whether the user has selected this answer.
|
||||
* @property isDisclosed whether the votes for this answer should be disclosed.
|
||||
* @property votesCount the number of votes for this answer.
|
||||
* @property progress the percentage of votes for this answer.
|
||||
*/
|
||||
data class PollAnswerItem(
|
||||
val answer: PollAnswer,
|
||||
val isSelected: Boolean,
|
||||
val isDisclosed: Boolean,
|
||||
val votesCount: Int,
|
||||
val progress: Float,
|
||||
)
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.constraintlayout.compose.Dimension
|
||||
import androidx.constraintlayout.compose.Visibility
|
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.RadioButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import io.element.android.libraries.ui.strings.CommonPlurals
|
||||
|
||||
@Suppress("DestructuringDeclarationWithTooManyEntries") // This is necessary to declare the constraints ids
|
||||
@Composable
|
||||
fun PollAnswerView(
|
||||
answerItem: PollAnswerItem,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ConstraintLayout(
|
||||
modifier
|
||||
.wrapContentHeight()
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = answerItem.isSelected,
|
||||
onClick = onClick,
|
||||
role = Role.RadioButton,
|
||||
)
|
||||
) {
|
||||
val (radioButton, answerText, votesText, progressBar) = createRefs()
|
||||
RadioButton(
|
||||
modifier = Modifier.constrainAs(radioButton) {
|
||||
top.linkTo(answerText.top)
|
||||
bottom.linkTo(answerText.bottom)
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(answerText.start)
|
||||
},
|
||||
selected = answerItem.isSelected,
|
||||
onClick = null // null recommended for accessibility with screenreaders
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.constrainAs(answerText) {
|
||||
width = Dimension.fillToConstraints
|
||||
top.linkTo(parent.top)
|
||||
start.linkTo(radioButton.end, margin = 8.dp)
|
||||
end.linkTo(votesText.start)
|
||||
bottom.linkTo(progressBar.top)
|
||||
},
|
||||
text = answerItem.answer.text,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.constrainAs(votesText) {
|
||||
start.linkTo(answerText.end)
|
||||
end.linkTo(parent.end)
|
||||
bottom.linkTo(answerText.bottom)
|
||||
visibility = if (answerItem.isDisclosed) Visibility.Visible else Visibility.Gone
|
||||
},
|
||||
text = pluralStringResource(
|
||||
id = CommonPlurals.common_poll_votes_count,
|
||||
count = answerItem.votesCount,
|
||||
answerItem.votesCount
|
||||
),
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
LinearProgressIndicator(
|
||||
progress = answerItem.progress,
|
||||
modifier = Modifier
|
||||
.constrainAs(progressBar) {
|
||||
start.linkTo(answerText.start)
|
||||
end.linkTo(votesText.end)
|
||||
top.linkTo(answerText.bottom, margin = 10.dp)
|
||||
bottom.linkTo(parent.bottom)
|
||||
width = Dimension.fillToConstraints
|
||||
visibility = if (answerItem.isDisclosed) Visibility.Visible else Visibility.Gone
|
||||
|
||||
},
|
||||
strokeCap = StrokeCap.Round,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
internal fun PollAnswerViewNoResultsPreview() = ElementPreview {
|
||||
PollAnswerView(
|
||||
answerItem = aPollAnswerItem(),
|
||||
onClick = { },
|
||||
)
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
internal fun PollAnswerViewWithResultPreview() = ElementPreview {
|
||||
PollAnswerView(
|
||||
answerItem = aPollAnswerItem(isDisclosed = true),
|
||||
onClick = { }
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
fun aPollAnswerItemList(isDisclosed: Boolean = true) = persistentListOf(
|
||||
aPollAnswerItem(
|
||||
answer = PollAnswer("option_1", "Italian \uD83C\uDDEE\uD83C\uDDF9"),
|
||||
isDisclosed = isDisclosed,
|
||||
votesCount = 5,
|
||||
progress = 0.5f
|
||||
),
|
||||
aPollAnswerItem(
|
||||
answer = PollAnswer("option_2", "Chinese \uD83C\uDDE8\uD83C\uDDF3"),
|
||||
isDisclosed = isDisclosed,
|
||||
votesCount = 0,
|
||||
progress = 0f
|
||||
),
|
||||
aPollAnswerItem(
|
||||
answer = PollAnswer("option_3", "Brazilian \uD83C\uDDE7\uD83C\uDDF7"),
|
||||
isDisclosed = isDisclosed,
|
||||
isSelected = true,
|
||||
votesCount = 1,
|
||||
progress = 0.1f
|
||||
),
|
||||
aPollAnswerItem(isDisclosed = isDisclosed),
|
||||
)
|
||||
|
||||
fun aPollAnswerItem(
|
||||
answer: PollAnswer = PollAnswer(
|
||||
"option_4",
|
||||
"French \uD83C\uDDEB\uD83C\uDDF7 But make it a very very very long option then this should just keep expanding"
|
||||
),
|
||||
isSelected: Boolean = false,
|
||||
isDisclosed: Boolean = true,
|
||||
votesCount: Int = 4,
|
||||
progress: Float = 0.4f,
|
||||
) = PollAnswerItem(
|
||||
answer = answer,
|
||||
isSelected = isSelected,
|
||||
isDisclosed = isDisclosed,
|
||||
votesCount = votesCount,
|
||||
progress = progress
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
|
||||
interface PollEntryPoint : FeatureEntryPoint {
|
||||
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
|
||||
interface NodeBuilder {
|
||||
fun callback(callback: Callback): NodeBuilder
|
||||
fun build(): Node
|
||||
}
|
||||
|
||||
interface Callback : Plugin {
|
||||
// Add your callbacks
|
||||
}
|
||||
}
|
||||
|
||||
52
features/poll/impl/build.gradle.kts
Normal file
52
features/poll/impl/build.gradle.kts
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
|
||||
@Suppress("DSL_SCOPE_VIOLATION")
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.ksp)
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.poll.impl"
|
||||
}
|
||||
|
||||
anvil {
|
||||
generateDaggerFactories.set(true)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
api(projects.features.poll.api)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.poll.api.PollEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultPollEntryPoint @Inject constructor() : PollEntryPoint {
|
||||
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): PollEntryPoint.NodeBuilder {
|
||||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
return object : PollEntryPoint.NodeBuilder {
|
||||
|
||||
override fun callback(callback: PollEntryPoint.Callback): PollEntryPoint.NodeBuilder {
|
||||
plugins += callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<PollFlowNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.composable.Children
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class PollFlowNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
) : BackstackNode<PollFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Root,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
) {
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Root : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Root -> {
|
||||
createNode(buildContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
Children(
|
||||
navModel = backstack,
|
||||
modifier = modifier,
|
||||
transitionHandler = rememberDefaultTransitionHandler(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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.libraries.designsystem.theme.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.ProgressIndicatorDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||
|
||||
@Composable
|
||||
fun LinearProgressIndicator(
|
||||
progress: Float,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = ProgressIndicatorDefaults.linearColor,
|
||||
trackColor: Color = ProgressIndicatorDefaults.linearTrackColor,
|
||||
strokeCap: StrokeCap = ProgressIndicatorDefaults.LinearStrokeCap,
|
||||
) {
|
||||
androidx.compose.material3.LinearProgressIndicator(
|
||||
modifier = modifier,
|
||||
progress = progress,
|
||||
color = color,
|
||||
trackColor = trackColor,
|
||||
strokeCap = strokeCap,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LinearProgressIndicator(
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = ProgressIndicatorDefaults.linearColor,
|
||||
trackColor: Color = ProgressIndicatorDefaults.linearTrackColor,
|
||||
strokeCap: StrokeCap = ProgressIndicatorDefaults.LinearStrokeCap,
|
||||
) {
|
||||
if (LocalInspectionMode.current) {
|
||||
// Use a determinate progress indicator to improve the preview rendering
|
||||
androidx.compose.material3.LinearProgressIndicator(
|
||||
modifier = modifier,
|
||||
progress = 0.75F,
|
||||
color = color,
|
||||
trackColor = trackColor,
|
||||
strokeCap = strokeCap,
|
||||
)
|
||||
} else {
|
||||
androidx.compose.material3.LinearProgressIndicator(
|
||||
modifier = modifier,
|
||||
color = color,
|
||||
trackColor = trackColor,
|
||||
strokeCap = strokeCap,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(group = PreviewGroup.Progress)
|
||||
@Composable
|
||||
internal fun LinearProgressIndicatorPreview() = ElementThemedPreview(vertical = false) { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
// Indeterminate progress
|
||||
LinearProgressIndicator(
|
||||
)
|
||||
// Fixed progress
|
||||
LinearProgressIndicator(
|
||||
progress = 0.90F
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessa
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollEndContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
@@ -94,6 +96,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
|
||||
is StateContent -> {
|
||||
stateContentFormatter.format(content, senderDisplayName, isOutgoing, RenderingMode.RoomList)
|
||||
}
|
||||
is PollContent, is PollEndContent, // TODO Polls: handle last message
|
||||
is FailedToParseMessageLikeContent, is FailedToParseStateContent, is UnknownContent -> {
|
||||
prefixIfNeeded(sp.getString(CommonStrings.common_unsupported_event), senderDisplayName, isDmRoom)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventTimeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollEndContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
@@ -63,6 +65,8 @@ class DefaultTimelineEventFormatter @Inject constructor(
|
||||
}
|
||||
RedactedContent,
|
||||
is StickerContent,
|
||||
is PollContent,
|
||||
is PollEndContent,
|
||||
is UnableToDecryptContent,
|
||||
is MessageContent,
|
||||
is FailedToParseMessageLikeContent,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.libraries.matrix.api.poll
|
||||
|
||||
data class PollAnswer(
|
||||
val id: String,
|
||||
val text: String
|
||||
)
|
||||
@@ -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.libraries.matrix.api.poll
|
||||
|
||||
enum class PollKind {
|
||||
/** Voters should see results as soon as they have voted. */
|
||||
Disclosed,
|
||||
|
||||
/** Results should be only revealed when the poll is ended. */
|
||||
Undisclosed
|
||||
}
|
||||
@@ -23,6 +23,8 @@ import io.element.android.libraries.matrix.api.media.FileInfo
|
||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
|
||||
sealed interface EventContent
|
||||
|
||||
@@ -69,6 +71,19 @@ data class StickerContent(
|
||||
val url: String
|
||||
) : EventContent
|
||||
|
||||
data class PollContent(
|
||||
val question: String,
|
||||
val kind: PollKind,
|
||||
val maxSelections: ULong,
|
||||
val answers: List<PollAnswer>,
|
||||
val votes: Map<String, List<UserId>>,
|
||||
val endTime: ULong?
|
||||
) : EventContent
|
||||
|
||||
data class PollEndContent(
|
||||
val startEventId: String
|
||||
) : EventContent
|
||||
|
||||
data class UnableToDecryptContent(
|
||||
val data: Data
|
||||
) : EventContent {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.libraries.matrix.impl.poll
|
||||
|
||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||
import org.matrix.rustcomponents.sdk.PollAnswer as RustPollAnswer
|
||||
|
||||
fun RustPollAnswer.map(): PollAnswer = PollAnswer(
|
||||
id = id,
|
||||
text = text,
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.libraries.matrix.impl.poll
|
||||
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
import org.matrix.rustcomponents.sdk.PollKind as RustPollKind
|
||||
|
||||
fun RustPollKind.map(): PollKind = when (this) {
|
||||
RustPollKind.DISCLOSED -> PollKind.Disclosed
|
||||
RustPollKind.UNDISCLOSED -> PollKind.Undisclosed
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParse
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollEndContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
@@ -30,6 +32,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.StickerConten
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
||||
import io.element.android.libraries.matrix.impl.media.map
|
||||
import io.element.android.libraries.matrix.impl.poll.map
|
||||
import org.matrix.rustcomponents.sdk.TimelineItemContent
|
||||
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
|
||||
import org.matrix.rustcomponents.sdk.EncryptedMessage as RustEncryptedMessage
|
||||
@@ -91,6 +94,21 @@ class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMap
|
||||
url = kind.url,
|
||||
)
|
||||
}
|
||||
is TimelineItemContentKind.Poll -> {
|
||||
PollContent(
|
||||
question = kind.question,
|
||||
kind = kind.kind.map(),
|
||||
maxSelections = kind.maxSelections,
|
||||
answers = kind.answers.map { answer -> answer.map() },
|
||||
votes = kind.votes.mapValues { vote ->
|
||||
vote.value.map { userId -> UserId(userId) }
|
||||
},
|
||||
endTime = kind.endTime,
|
||||
)
|
||||
}
|
||||
is TimelineItemContentKind.PollEnd -> {
|
||||
PollEndContent(startEventId = kind.startEventId)
|
||||
}
|
||||
is TimelineItemContentKind.UnableToDecrypt -> {
|
||||
UnableToDecryptContent(
|
||||
data = kind.msg.map()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user