"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:
parent
218af83b54
commit
f2d1658ffa
50 changed files with 1085 additions and 1 deletions
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)
|
anvil(projects.anvilcodegen)
|
||||||
api(projects.features.messages.api)
|
api(projects.features.messages.api)
|
||||||
implementation(projects.features.location.api)
|
implementation(projects.features.location.api)
|
||||||
|
implementation(projects.features.poll.api)
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.architecture)
|
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.TimelineItemFileContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
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.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.TimelineItemRedactedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||||
|
|
@ -277,6 +278,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
is TimelineItemLocationContent -> AttachmentThumbnailInfo(
|
is TimelineItemLocationContent -> AttachmentThumbnailInfo(
|
||||||
type = AttachmentThumbnailType.Location,
|
type = AttachmentThumbnailType.Location,
|
||||||
)
|
)
|
||||||
|
is TimelineItemPollContent, // TODO Polls: handle reply to
|
||||||
is TimelineItemTextBasedContent,
|
is TimelineItemTextBasedContent,
|
||||||
is TimelineItemRedactedContent,
|
is TimelineItemRedactedContent,
|
||||||
is TimelineItemStateContent,
|
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.TimelineItemFileContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
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.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.TimelineItemRedactedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
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) }
|
val textContent = remember(event.content) { formatter.format(event) }
|
||||||
|
|
||||||
when (event.content) {
|
when (event.content) {
|
||||||
|
is TimelineItemPollContent, // TODO Polls: handle summary
|
||||||
is TimelineItemTextBasedContent,
|
is TimelineItemTextBasedContent,
|
||||||
is TimelineItemStateContent,
|
is TimelineItemStateContent,
|
||||||
is TimelineItemEncryptedContent,
|
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.TimelineItemFileContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
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.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.TimelineItemRedactedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||||
|
|
@ -90,5 +91,10 @@ fun TimelineItemEventContentView(
|
||||||
content = content,
|
content = content,
|
||||||
modifier = modifier
|
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.FailedToParseMessageLikeContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
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.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.ProfileChangeContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
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 messageFactory: TimelineItemContentMessageFactory,
|
||||||
private val redactedMessageFactory: TimelineItemContentRedactedFactory,
|
private val redactedMessageFactory: TimelineItemContentRedactedFactory,
|
||||||
private val stickerFactory: TimelineItemContentStickerFactory,
|
private val stickerFactory: TimelineItemContentStickerFactory,
|
||||||
|
private val pollFactory: TimelineItemContentPollFactory,
|
||||||
|
private val pollEndFactory: TimelineItemContentPollEndFactory,
|
||||||
private val utdFactory: TimelineItemContentUTDFactory,
|
private val utdFactory: TimelineItemContentUTDFactory,
|
||||||
private val roomMembershipFactory: TimelineItemContentRoomMembershipFactory,
|
private val roomMembershipFactory: TimelineItemContentRoomMembershipFactory,
|
||||||
private val profileChangeFactory: TimelineItemContentProfileChangeFactory,
|
private val profileChangeFactory: TimelineItemContentProfileChangeFactory,
|
||||||
|
|
@ -53,6 +57,8 @@ class TimelineItemContentFactory @Inject constructor(
|
||||||
is RoomMembershipContent -> roomMembershipFactory.create(eventTimelineItem)
|
is RoomMembershipContent -> roomMembershipFactory.create(eventTimelineItem)
|
||||||
is StateContent -> stateFactory.create(eventTimelineItem)
|
is StateContent -> stateFactory.create(eventTimelineItem)
|
||||||
is StickerContent -> stickerFactory.create(itemContent)
|
is StickerContent -> stickerFactory.create(itemContent)
|
||||||
|
is PollContent -> pollFactory.create(itemContent)
|
||||||
|
is PollEndContent -> pollEndFactory.create(itemContent)
|
||||||
is UnableToDecryptContent -> utdFactory.create(itemContent)
|
is UnableToDecryptContent -> utdFactory.create(itemContent)
|
||||||
is UnknownContent -> TimelineItemUnknownContent
|
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.TimelineItemFileContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
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.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.TimelineItemProfileChangeContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRoomMembershipContent
|
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.FailedToParseMessageLikeContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
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.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.ProfileChangeContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||||
|
|
@ -55,6 +58,7 @@ internal fun TimelineItem.Event.canBeGrouped(): Boolean {
|
||||||
is TimelineItemVideoContent,
|
is TimelineItemVideoContent,
|
||||||
is TimelineItemAudioContent,
|
is TimelineItemAudioContent,
|
||||||
is TimelineItemLocationContent,
|
is TimelineItemLocationContent,
|
||||||
|
is TimelineItemPollContent,
|
||||||
TimelineItemRedactedContent,
|
TimelineItemRedactedContent,
|
||||||
TimelineItemUnknownContent -> false
|
TimelineItemUnknownContent -> false
|
||||||
is TimelineItemProfileChangeContent,
|
is TimelineItemProfileChangeContent,
|
||||||
|
|
@ -74,6 +78,8 @@ internal fun MatrixTimelineItem.Event.canBeDisplayedInBubbleBlock(): Boolean {
|
||||||
is MessageContent,
|
is MessageContent,
|
||||||
RedactedContent,
|
RedactedContent,
|
||||||
is StickerContent,
|
is StickerContent,
|
||||||
|
is PollContent,
|
||||||
|
is PollEndContent,
|
||||||
is UnableToDecryptContent -> true
|
is UnableToDecryptContent -> true
|
||||||
is FailedToParseStateContent,
|
is FailedToParseStateContent,
|
||||||
is ProfileChangeContent,
|
is ProfileChangeContent,
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ fun TimelineItemEventContent.canReact(): Boolean =
|
||||||
is TimelineItemFileContent,
|
is TimelineItemFileContent,
|
||||||
is TimelineItemImageContent,
|
is TimelineItemImageContent,
|
||||||
is TimelineItemLocationContent,
|
is TimelineItemLocationContent,
|
||||||
|
is TimelineItemPollContent,
|
||||||
is TimelineItemVideoContent -> true
|
is TimelineItemVideoContent -> true
|
||||||
is TimelineItemStateContent,
|
is TimelineItemStateContent,
|
||||||
is TimelineItemRedactedContent,
|
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.TimelineItemFileContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
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.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.TimelineItemProfileChangeContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
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.TimelineItemStateContent
|
||||||
|
|
@ -47,6 +48,7 @@ class MessageSummaryFormatterImpl @Inject constructor(
|
||||||
is TimelineItemLocationContent -> context.getString(CommonStrings.common_shared_location)
|
is TimelineItemLocationContent -> context.getString(CommonStrings.common_shared_location)
|
||||||
is TimelineItemEncryptedContent -> context.getString(CommonStrings.common_unable_to_decrypt)
|
is TimelineItemEncryptedContent -> context.getString(CommonStrings.common_unable_to_decrypt)
|
||||||
is TimelineItemRedactedContent -> context.getString(CommonStrings.common_message_removed)
|
is TimelineItemRedactedContent -> context.getString(CommonStrings.common_message_removed)
|
||||||
|
is TimelineItemPollContent, // Todo Polls: handle summary
|
||||||
is TimelineItemUnknownContent -> context.getString(CommonStrings.common_unsupported_event)
|
is TimelineItemUnknownContent -> context.getString(CommonStrings.common_unsupported_event)
|
||||||
is TimelineItemImageContent -> context.getString(CommonStrings.common_image)
|
is TimelineItemImageContent -> context.getString(CommonStrings.common_image)
|
||||||
is TimelineItemVideoContent -> context.getString(CommonStrings.common_video)
|
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.TimelineItemContentFailedToParseMessageFactory
|
||||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFailedToParseStateFactory
|
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.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.TimelineItemContentProfileChangeFactory
|
||||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentRedactedFactory
|
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentRedactedFactory
|
||||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentRoomMembershipFactory
|
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 {
|
internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
|
||||||
val timelineEventFormatter = aTimelineEventFormatter()
|
val timelineEventFormatter = aTimelineEventFormatter()
|
||||||
|
val matrixClient = FakeMatrixClient()
|
||||||
return TimelineItemsFactory(
|
return TimelineItemsFactory(
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
eventItemFactory = TimelineItemEventFactory(
|
eventItemFactory = TimelineItemEventFactory(
|
||||||
|
|
@ -49,6 +52,8 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
|
||||||
messageFactory = TimelineItemContentMessageFactory(FakeFileSizeFormatter(), FileExtensionExtractorWithoutValidation()),
|
messageFactory = TimelineItemContentMessageFactory(FakeFileSizeFormatter(), FileExtensionExtractorWithoutValidation()),
|
||||||
redactedMessageFactory = TimelineItemContentRedactedFactory(),
|
redactedMessageFactory = TimelineItemContentRedactedFactory(),
|
||||||
stickerFactory = TimelineItemContentStickerFactory(),
|
stickerFactory = TimelineItemContentStickerFactory(),
|
||||||
|
pollFactory = TimelineItemContentPollFactory(matrixClient),
|
||||||
|
pollEndFactory = TimelineItemContentPollEndFactory(),
|
||||||
utdFactory = TimelineItemContentUTDFactory(),
|
utdFactory = TimelineItemContentUTDFactory(),
|
||||||
roomMembershipFactory = TimelineItemContentRoomMembershipFactory(timelineEventFormatter),
|
roomMembershipFactory = TimelineItemContentRoomMembershipFactory(timelineEventFormatter),
|
||||||
profileChangeFactory = TimelineItemContentProfileChangeFactory(timelineEventFormatter),
|
profileChangeFactory = TimelineItemContentProfileChangeFactory(timelineEventFormatter),
|
||||||
|
|
@ -56,7 +61,7 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
|
||||||
failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(),
|
failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(),
|
||||||
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory()
|
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory()
|
||||||
),
|
),
|
||||||
matrixClient = FakeMatrixClient(),
|
matrixClient = matrixClient,
|
||||||
),
|
),
|
||||||
virtualItemFactory = TimelineItemVirtualFactory(
|
virtualItemFactory = TimelineItemVirtualFactory(
|
||||||
daySeparatorFactory = TimelineItemDaySeparatorFactory(
|
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.MessageContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
|
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.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.ProfileChangeContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||||
|
|
@ -94,6 +96,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
|
||||||
is StateContent -> {
|
is StateContent -> {
|
||||||
stateContentFormatter.format(content, senderDisplayName, isOutgoing, RenderingMode.RoomList)
|
stateContentFormatter.format(content, senderDisplayName, isOutgoing, RenderingMode.RoomList)
|
||||||
}
|
}
|
||||||
|
is PollContent, is PollEndContent, // TODO Polls: handle last message
|
||||||
is FailedToParseMessageLikeContent, is FailedToParseStateContent, is UnknownContent -> {
|
is FailedToParseMessageLikeContent, is FailedToParseStateContent, is UnknownContent -> {
|
||||||
prefixIfNeeded(sp.getString(CommonStrings.common_unsupported_event), senderDisplayName, isDmRoom)
|
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.FailedToParseMessageLikeContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
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.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.ProfileChangeContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||||
|
|
@ -63,6 +65,8 @@ class DefaultTimelineEventFormatter @Inject constructor(
|
||||||
}
|
}
|
||||||
RedactedContent,
|
RedactedContent,
|
||||||
is StickerContent,
|
is StickerContent,
|
||||||
|
is PollContent,
|
||||||
|
is PollEndContent,
|
||||||
is UnableToDecryptContent,
|
is UnableToDecryptContent,
|
||||||
is MessageContent,
|
is MessageContent,
|
||||||
is FailedToParseMessageLikeContent,
|
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.ImageInfo
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
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.media.VideoInfo
|
||||||
|
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||||
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
|
|
||||||
sealed interface EventContent
|
sealed interface EventContent
|
||||||
|
|
||||||
|
|
@ -69,6 +71,19 @@ data class StickerContent(
|
||||||
val url: String
|
val url: String
|
||||||
) : EventContent
|
) : 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(
|
data class UnableToDecryptContent(
|
||||||
val data: Data
|
val data: Data
|
||||||
) : EventContent {
|
) : 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.FailedToParseStateContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
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.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.ProfileChangeContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
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.UnableToDecryptContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
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.media.map
|
||||||
|
import io.element.android.libraries.matrix.impl.poll.map
|
||||||
import org.matrix.rustcomponents.sdk.TimelineItemContent
|
import org.matrix.rustcomponents.sdk.TimelineItemContent
|
||||||
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
|
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
|
||||||
import org.matrix.rustcomponents.sdk.EncryptedMessage as RustEncryptedMessage
|
import org.matrix.rustcomponents.sdk.EncryptedMessage as RustEncryptedMessage
|
||||||
|
|
@ -91,6 +94,21 @@ class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMap
|
||||||
url = kind.url,
|
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 -> {
|
is TimelineItemContentKind.UnableToDecrypt -> {
|
||||||
UnableToDecryptContent(
|
UnableToDecryptContent(
|
||||||
data = kind.msg.map()
|
data = kind.msg.map()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1978bafb5fd079de3fe7945a0574b2b19b0480d9d7d146e034b749e674f76254
|
||||||
|
size 48667
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1978bafb5fd079de3fe7945a0574b2b19b0480d9d7d146e034b749e674f76254
|
||||||
|
size 48667
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:df79a0bd1caa8ecf4b701eaa589cc90efc748b986388f9ae5636b9dceba4492c
|
||||||
|
size 45928
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:df79a0bd1caa8ecf4b701eaa589cc90efc748b986388f9ae5636b9dceba4492c
|
||||||
|
size 45928
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1fb1d9bb4ab01a0af8ee20c08084fd01baba0f77a4c4a39c179a8e19d1dadd47
|
||||||
|
size 46624
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:18749d3e3a8b4e1176fa6c8303439d07306c3518e6be625d5ec236e2f24433b5
|
||||||
|
size 43265
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1978bafb5fd079de3fe7945a0574b2b19b0480d9d7d146e034b749e674f76254
|
||||||
|
size 48667
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:df79a0bd1caa8ecf4b701eaa589cc90efc748b986388f9ae5636b9dceba4492c
|
||||||
|
size 45928
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:239eaab2812e0e2968f52a41a741b8782b28ceaf72f029267de6248a5ce67fc6
|
||||||
|
size 23063
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a9cf7435b528b3920e621d6bd688a8692c8bd366db1522d2947d5e204f905644
|
||||||
|
size 21563
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:239eaab2812e0e2968f52a41a741b8782b28ceaf72f029267de6248a5ce67fc6
|
||||||
|
size 23063
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a9cf7435b528b3920e621d6bd688a8692c8bd366db1522d2947d5e204f905644
|
||||||
|
size 21563
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b756303958d8234c0e8c0f9ca16335c54d8bd610c73790058d6389eb7c8c3fae
|
||||||
|
size 4875
|
||||||
Loading…
Add table
Add a link
Reference in a new issue