Polls: share logic around PollContent
This commit is contained in:
parent
d0d73e04c1
commit
4a2cbb1ed4
31 changed files with 571 additions and 149 deletions
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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.actions
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import im.vector.app.features.analytics.plan.PollEnd
|
||||
import io.element.android.features.poll.api.actions.EndPollAction
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(RoomScope::class)
|
||||
class DefaultEndPollAction @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : EndPollAction {
|
||||
|
||||
override suspend fun execute(pollStartId: EventId): Result<Unit> {
|
||||
return room.endPoll(
|
||||
pollStartId = pollStartId,
|
||||
text = "The poll with event id: $pollStartId has ended."
|
||||
).onSuccess {
|
||||
analyticsService.capture(PollEnd())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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.actions
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import im.vector.app.features.analytics.plan.PollVote
|
||||
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(RoomScope::class)
|
||||
class DefaultSendPollResponseAction @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : SendPollResponseAction {
|
||||
|
||||
override suspend fun execute(pollStartId: EventId, answerId: String): Result<Unit> {
|
||||
return room.sendPollResponse(
|
||||
pollStartId = pollStartId,
|
||||
answers = listOf(answerId),
|
||||
).onSuccess {
|
||||
analyticsService.capture(PollVote())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -16,6 +16,11 @@
|
|||
|
||||
package io.element.android.features.poll.impl.history
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
||||
sealed interface PollHistoryEvents {
|
||||
data object History : PollHistoryEvents
|
||||
data object LoadMore : PollHistoryEvents
|
||||
data class PollAnswerSelected(val pollStartId: EventId, val answerId: String) : PollHistoryEvents
|
||||
data class PollEndClicked(val pollStartId: EventId) : PollHistoryEvents
|
||||
data object EditPoll : PollHistoryEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.history
|
||||
|
||||
import io.element.android.features.poll.api.pollcontent.PollContentState
|
||||
|
||||
sealed interface PollHistoryItem {
|
||||
data class PollContent(val formattedDate: String, val state: PollContentState) : PollHistoryItem
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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.history
|
||||
|
||||
import io.element.android.features.poll.api.pollcontent.PollContentStateFactory
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class PollHistoryItemsFactory @Inject constructor(
|
||||
private val pollContentStateFactory: PollContentStateFactory,
|
||||
private val daySeparatorFormatter: DaySeparatorFormatter,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
|
||||
suspend fun create(timelineItems: List<MatrixTimelineItem>): List<PollHistoryItem> = withContext(dispatchers.computation) {
|
||||
timelineItems.mapNotNull { create(it) }.reversed()
|
||||
}
|
||||
|
||||
private suspend fun create(timelineItem: MatrixTimelineItem): PollHistoryItem? {
|
||||
return when (timelineItem) {
|
||||
is MatrixTimelineItem.Event -> {
|
||||
val pollContent = timelineItem.event.content as? PollContent ?: return null
|
||||
val pollContentState = pollContentStateFactory.create(timelineItem.event, pollContent)
|
||||
PollHistoryItem.PollContent(
|
||||
formattedDate = daySeparatorFormatter.format(timelineItem.event.timestamp),
|
||||
state = pollContentState
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -17,22 +17,29 @@
|
|||
package io.element.android.features.poll.impl.history
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.poll.api.actions.EndPollAction
|
||||
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class PollHistoryPresenter @AssistedInject constructor(
|
||||
@Assisted private val pollHistory: MatrixTimeline,
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
private val sendPollResponseAction: SendPollResponseAction,
|
||||
private val endPollAction: EndPollAction,
|
||||
private val pollHistoryItemFactory: PollHistoryItemsFactory,
|
||||
) : Presenter<PollHistoryState> {
|
||||
|
||||
@AssistedFactory
|
||||
|
|
@ -47,26 +54,36 @@ class PollHistoryPresenter @AssistedInject constructor(
|
|||
val paginationState by pollHistory.paginationState.collectAsState()
|
||||
val timelineItemsFlow = remember {
|
||||
pollHistory.timelineItems.map { items ->
|
||||
items.filterIsInstance<MatrixTimelineItem.Event>()
|
||||
.map { it.event.content }
|
||||
.filterIsInstance<PollContent>()
|
||||
.reversed()
|
||||
}.onEach {
|
||||
if (it.isEmpty()) pollHistory.paginateBackwards(20, 50)
|
||||
pollHistoryItemFactory.create(items)
|
||||
}
|
||||
}
|
||||
val items by timelineItemsFlow.collectAsState(initial = emptyList())
|
||||
|
||||
LaunchedEffect(items.size) {
|
||||
if (items.isEmpty()) loadMore()
|
||||
}
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
fun handleEvents(event: PollHistoryEvents) {
|
||||
when (event) {
|
||||
is PollHistoryEvents.History -> Unit // TODO which events to handle?
|
||||
is PollHistoryEvents.LoadMore -> {
|
||||
coroutineScope.loadMore()
|
||||
}
|
||||
is PollHistoryEvents.PollAnswerSelected -> appCoroutineScope.launch {
|
||||
sendPollResponseAction.execute(pollStartId = event.pollStartId, answerId = event.answerId)
|
||||
}
|
||||
is PollHistoryEvents.PollEndClicked -> appCoroutineScope.launch {
|
||||
endPollAction.execute(pollStartId = event.pollStartId)
|
||||
}
|
||||
PollHistoryEvents.EditPoll -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
return PollHistoryState(
|
||||
paginationState = paginationState,
|
||||
matrixTimelineItems = items.toImmutableList(),
|
||||
pollItems = items.toImmutableList(),
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.loadMore() = launch {
|
||||
pollHistory.paginateBackwards(20, 3)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,10 @@
|
|||
package io.element.android.features.poll.impl.history
|
||||
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class PollHistoryState(
|
||||
val paginationState: MatrixTimeline.PaginationState,
|
||||
val matrixTimelineItems: ImmutableList<PollContent>,
|
||||
val pollItems: ImmutableList<PollHistoryItem>,
|
||||
val eventSink: (PollHistoryEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,27 +16,31 @@
|
|||
|
||||
package io.element.android.features.poll.impl.history
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.poll.api.PollAnswerItem
|
||||
import io.element.android.features.poll.api.PollContentView
|
||||
import io.element.android.features.poll.api.pollcontent.PollContentView
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
|
@ -70,37 +74,75 @@ fun PollHistoryView(
|
|||
if (state.paginationState.isBackPaginating) item {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
items(state.matrixTimelineItems) { pollContent ->
|
||||
PollContentView(
|
||||
eventId = null,
|
||||
question = pollContent.question,
|
||||
answerItems = pollContent.answers.map {
|
||||
PollAnswerItem(
|
||||
answer = PollAnswer(
|
||||
id = it.id,
|
||||
text = it.text,
|
||||
),
|
||||
isSelected = false,
|
||||
isEnabled = false,
|
||||
isWinner = false,
|
||||
isDisclosed = false,
|
||||
votesCount = 9393,
|
||||
percentage = 4.5f,
|
||||
)
|
||||
}.toImmutableList(),
|
||||
pollKind = pollContent.kind,
|
||||
isPollEditable = false,
|
||||
isPollEnded = false,
|
||||
isMine = false,
|
||||
onAnswerSelected = { _, _ -> },
|
||||
onPollEdit = {},
|
||||
onPollEnd = {},
|
||||
itemsIndexed(state.pollItems) { index, pollHistoryItem ->
|
||||
PollHistoryItemRow(
|
||||
pollHistoryItem = pollHistoryItem,
|
||||
onAnswerSelected = fun(pollStartId: EventId, answerId: String) {
|
||||
state.eventSink(PollHistoryEvents.PollAnswerSelected(pollStartId, answerId))
|
||||
},
|
||||
onPollEdit = {
|
||||
state.eventSink(PollHistoryEvents.EditPoll)
|
||||
},
|
||||
onPollEnd = {
|
||||
state.eventSink(PollHistoryEvents.PollEndClicked(it))
|
||||
},
|
||||
)
|
||||
if (index != state.pollItems.lastIndex) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PollHistoryItemRow(
|
||||
pollHistoryItem: PollHistoryItem,
|
||||
onAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit,
|
||||
onPollEdit: (pollStartId: EventId) -> Unit,
|
||||
onPollEnd: (pollStartId: EventId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (pollHistoryItem) {
|
||||
is PollHistoryItem.PollContent -> {
|
||||
PollContentItemRow(
|
||||
pollContentItem = pollHistoryItem,
|
||||
onAnswerSelected = onAnswerSelected,
|
||||
onPollEdit = onPollEdit,
|
||||
onPollEnd = onPollEnd,
|
||||
modifier = modifier.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 24.dp
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PollContentItemRow(
|
||||
pollContentItem: PollHistoryItem.PollContent,
|
||||
onAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit,
|
||||
onPollEdit: (pollStartId: EventId) -> Unit,
|
||||
onPollEnd: (pollStartId: EventId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Text(
|
||||
text = pollContentItem.formattedDate,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
PollContentView(
|
||||
state = pollContentItem.state,
|
||||
onAnswerSelected = onAnswerSelected,
|
||||
onPollEdit = onPollEdit,
|
||||
onPollEnd = onPollEnd,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun PollHistoryViewPreview(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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.model
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.poll.api.pollcontent.PollAnswerItem
|
||||
import io.element.android.features.poll.api.pollcontent.PollContentState
|
||||
import io.element.android.features.poll.api.pollcontent.PollContentStateFactory
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.poll.isDisclosed
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(RoomScope::class)
|
||||
class DefaultPollContentStateFactory @Inject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
) : PollContentStateFactory {
|
||||
|
||||
override suspend fun create(
|
||||
event: EventTimelineItem,
|
||||
content: PollContent
|
||||
): PollContentState {
|
||||
val totalVoteCount = content.votes.flatMap { it.value }.size
|
||||
val myVotes = content.votes.filter { matrixClient.sessionId in it.value }.keys
|
||||
val isPollEnded = content.endTime != null
|
||||
val winnerIds = if (!isPollEnded) {
|
||||
emptyList()
|
||||
} else {
|
||||
content.answers
|
||||
.map { answer -> answer.id }
|
||||
.groupBy { answerId -> content.votes[answerId]?.size ?: 0 } // Group by votes count
|
||||
.maxByOrNull { (votes, _) -> votes } // Keep max voted answers
|
||||
?.takeIf { (votes, _) -> votes > 0 } // Ignore if no option has been voted
|
||||
?.value
|
||||
.orEmpty()
|
||||
}
|
||||
val answerItems = content.answers.map { answer ->
|
||||
val answerVoteCount = content.votes[answer.id]?.size ?: 0
|
||||
val isSelected = answer.id in myVotes
|
||||
val isWinner = answer.id in winnerIds
|
||||
val percentage = if (totalVoteCount > 0) answerVoteCount.toFloat() / totalVoteCount.toFloat() else 0f
|
||||
PollAnswerItem(
|
||||
answer = answer,
|
||||
isSelected = isSelected,
|
||||
isEnabled = !isPollEnded,
|
||||
isWinner = isWinner,
|
||||
isDisclosed = content.kind.isDisclosed || isPollEnded,
|
||||
votesCount = answerVoteCount,
|
||||
percentage = percentage,
|
||||
)
|
||||
}
|
||||
|
||||
return PollContentState(
|
||||
eventId = event.eventId,
|
||||
isMine = event.isOwn,
|
||||
question = content.question,
|
||||
answerItems = answerItems.toImmutableList(),
|
||||
pollKind = content.kind,
|
||||
isPollEditable = event.isEditable,
|
||||
isPollEnded = isPollEnded,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue