Merge pull request #1913 from element-hq/julioromano/poll_history_entry_point
Poll history UI
This commit is contained in:
commit
4aad49d424
231 changed files with 2470 additions and 747 deletions
1
changelog.d/2014.feature
Normal file
1
changelog.d/2014.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Poll history of a room is now accessible from the room details screen.
|
||||||
|
|
@ -98,6 +98,8 @@ dependencies {
|
||||||
testImplementation(libs.test.mockk)
|
testImplementation(libs.test.mockk)
|
||||||
testImplementation(libs.test.junitext)
|
testImplementation(libs.test.junitext)
|
||||||
testImplementation(libs.test.robolectric)
|
testImplementation(libs.test.robolectric)
|
||||||
|
testImplementation(projects.features.poll.test)
|
||||||
|
testImplementation(projects.features.poll.impl)
|
||||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,14 +30,14 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.PollEnd
|
|
||||||
import im.vector.app.features.analytics.plan.PollVote
|
|
||||||
import io.element.android.features.messages.impl.MessagesNavigator
|
import io.element.android.features.messages.impl.MessagesNavigator
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
import io.element.android.features.messages.impl.timeline.session.SessionState
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
||||||
|
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.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
|
@ -53,7 +53,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemE
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
|
@ -70,11 +69,12 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
private val appScope: CoroutineScope,
|
private val appScope: CoroutineScope,
|
||||||
@Assisted private val navigator: MessagesNavigator,
|
@Assisted private val navigator: MessagesNavigator,
|
||||||
private val analyticsService: AnalyticsService,
|
|
||||||
private val verificationService: SessionVerificationService,
|
private val verificationService: SessionVerificationService,
|
||||||
private val encryptionService: EncryptionService,
|
private val encryptionService: EncryptionService,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
private val redactedVoiceMessageManager: RedactedVoiceMessageManager,
|
private val redactedVoiceMessageManager: RedactedVoiceMessageManager,
|
||||||
|
private val sendPollResponseAction: SendPollResponseAction,
|
||||||
|
private val endPollAction: EndPollAction,
|
||||||
) : Presenter<TimelineState> {
|
) : Presenter<TimelineState> {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
@ -133,18 +133,15 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is TimelineEvents.PollAnswerSelected -> appScope.launch {
|
is TimelineEvents.PollAnswerSelected -> appScope.launch {
|
||||||
room.sendPollResponse(
|
sendPollResponseAction.execute(
|
||||||
pollStartId = event.pollStartId,
|
pollStartId = event.pollStartId,
|
||||||
answers = listOf(event.answerId),
|
answerId = event.answerId
|
||||||
)
|
)
|
||||||
analyticsService.capture(PollVote())
|
|
||||||
}
|
}
|
||||||
is TimelineEvents.PollEndClicked -> appScope.launch {
|
is TimelineEvents.PollEndClicked -> appScope.launch {
|
||||||
room.endPoll(
|
endPollAction.execute(
|
||||||
pollStartId = event.pollStartId,
|
pollStartId = event.pollStartId,
|
||||||
text = "The poll with event id: ${event.pollStartId} has ended."
|
|
||||||
)
|
)
|
||||||
analyticsService.capture(PollEnd())
|
|
||||||
}
|
}
|
||||||
is TimelineEvents.PollEditClicked ->
|
is TimelineEvents.PollEditClicked ->
|
||||||
navigator.onEditPollClicked(event.pollStartId)
|
navigator.onEditPollClicked(event.pollStartId)
|
||||||
|
|
|
||||||
|
|
@ -521,8 +521,6 @@ private fun MessageEventBubbleContent(
|
||||||
) {
|
) {
|
||||||
TimelineItemEventContentView(
|
TimelineItemEventContentView(
|
||||||
content = event.content,
|
content = event.content,
|
||||||
isMine = event.isMine,
|
|
||||||
isEditable = event.isEditable,
|
|
||||||
onLinkClicked = { url ->
|
onLinkClicked = { url ->
|
||||||
when (val permalink = PermalinkParser.parse(Uri.parse(url))) {
|
when (val permalink = PermalinkParser.parse(Uri.parse(url))) {
|
||||||
is PermalinkData.UserLink -> {
|
is PermalinkData.UserLink -> {
|
||||||
|
|
|
||||||
|
|
@ -80,8 +80,6 @@ fun TimelineItemStateEventRow(
|
||||||
) {
|
) {
|
||||||
TimelineItemEventContentView(
|
TimelineItemEventContentView(
|
||||||
content = event.content,
|
content = event.content,
|
||||||
isMine = event.isMine,
|
|
||||||
isEditable = event.isEditable,
|
|
||||||
onLinkClicked = {},
|
onLinkClicked = {},
|
||||||
extraPadding = noExtraPadding,
|
extraPadding = noExtraPadding,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,6 @@ import io.element.android.libraries.architecture.Presenter
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemEventContentView(
|
fun TimelineItemEventContentView(
|
||||||
content: TimelineItemEventContent,
|
content: TimelineItemEventContent,
|
||||||
isMine: Boolean,
|
|
||||||
isEditable: Boolean,
|
|
||||||
extraPadding: ExtraPadding,
|
extraPadding: ExtraPadding,
|
||||||
onLinkClicked: (url: String) -> Unit,
|
onLinkClicked: (url: String) -> Unit,
|
||||||
eventSink: (TimelineEvents) -> Unit,
|
eventSink: (TimelineEvents) -> Unit,
|
||||||
|
|
@ -98,8 +96,6 @@ fun TimelineItemEventContentView(
|
||||||
)
|
)
|
||||||
is TimelineItemPollContent -> TimelineItemPollView(
|
is TimelineItemPollContent -> TimelineItemPollView(
|
||||||
content = content,
|
content = content,
|
||||||
isMine = isMine,
|
|
||||||
isEditable = isEditable,
|
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContentProvider
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContentProvider
|
||||||
import io.element.android.features.poll.api.PollContentView
|
import io.element.android.features.poll.api.pollcontent.PollContentView
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
@ -31,8 +31,6 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemPollView(
|
fun TimelineItemPollView(
|
||||||
content: TimelineItemPollContent,
|
content: TimelineItemPollContent,
|
||||||
isMine: Boolean,
|
|
||||||
isEditable: Boolean,
|
|
||||||
eventSink: (TimelineEvents) -> Unit,
|
eventSink: (TimelineEvents) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -54,8 +52,8 @@ fun TimelineItemPollView(
|
||||||
answerItems = content.answerItems.toImmutableList(),
|
answerItems = content.answerItems.toImmutableList(),
|
||||||
pollKind = content.pollKind,
|
pollKind = content.pollKind,
|
||||||
isPollEnded = content.isEnded,
|
isPollEnded = content.isEnded,
|
||||||
isPollEditable = isEditable,
|
isPollEditable = content.isEditable,
|
||||||
isMine = isMine,
|
isMine = content.isMine,
|
||||||
onAnswerSelected = ::onAnswerSelected,
|
onAnswerSelected = ::onAnswerSelected,
|
||||||
onPollEdit = ::onPollEdit,
|
onPollEdit = ::onPollEdit,
|
||||||
onPollEnd = ::onPollEnd,
|
onPollEnd = ::onPollEnd,
|
||||||
|
|
@ -69,20 +67,6 @@ internal fun TimelineItemPollViewPreview(@PreviewParameter(TimelineItemPollConte
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
TimelineItemPollView(
|
TimelineItemPollView(
|
||||||
content = content,
|
content = content,
|
||||||
isMine = false,
|
|
||||||
isEditable = false,
|
|
||||||
eventSink = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreviewsDayNight
|
|
||||||
@Composable
|
|
||||||
internal fun TimelineItemPollCreatorViewPreview(@PreviewParameter(TimelineItemPollContentProvider::class) content: TimelineItemPollContent) =
|
|
||||||
ElementPreview {
|
|
||||||
TimelineItemPollView(
|
|
||||||
content = content,
|
|
||||||
isMine = true,
|
|
||||||
isEditable = false,
|
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ 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, eventTimelineItem.eventId)
|
is PollContent -> pollFactory.create(eventTimelineItem, itemContent)
|
||||||
is UnableToDecryptContent -> utdFactory.create(itemContent)
|
is UnableToDecryptContent -> utdFactory.create(itemContent)
|
||||||
is UnknownContent -> TimelineItemUnknownContent
|
is UnknownContent -> TimelineItemUnknownContent
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,64 +19,33 @@ package io.element.android.features.messages.impl.timeline.factories.event
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||||
import io.element.android.features.poll.api.PollAnswerItem
|
import io.element.android.features.poll.api.pollcontent.PollContentStateFactory
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
|
||||||
import io.element.android.libraries.matrix.api.poll.isDisclosed
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TimelineItemContentPollFactory @Inject constructor(
|
class TimelineItemContentPollFactory @Inject constructor(
|
||||||
private val matrixClient: MatrixClient,
|
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val pollContentStateFactory: PollContentStateFactory,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun create(
|
suspend fun create(
|
||||||
|
event: EventTimelineItem,
|
||||||
content: PollContent,
|
content: PollContent,
|
||||||
eventId: EventId?
|
|
||||||
): TimelineItemEventContent {
|
): TimelineItemEventContent {
|
||||||
if (!featureFlagService.isFeatureEnabled(FeatureFlags.Polls)) return TimelineItemUnknownContent
|
if (!featureFlagService.isFeatureEnabled(FeatureFlags.Polls)) return TimelineItemUnknownContent
|
||||||
|
val pollContentState = pollContentStateFactory.create(event, content)
|
||||||
// Todo Move this computation to the matrix rust sdk
|
|
||||||
val totalVoteCount = content.votes.flatMap { it.value }.size
|
|
||||||
val myVotes = content.votes.filter { matrixClient.sessionId in it.value }.keys
|
|
||||||
val isEndedPoll = content.endTime != null
|
|
||||||
val winnerIds = if (!isEndedPoll) {
|
|
||||||
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 = !isEndedPoll,
|
|
||||||
isWinner = isWinner,
|
|
||||||
isDisclosed = content.kind.isDisclosed || isEndedPoll,
|
|
||||||
votesCount = answerVoteCount,
|
|
||||||
percentage = percentage,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return TimelineItemPollContent(
|
return TimelineItemPollContent(
|
||||||
eventId = eventId,
|
isMine = pollContentState.isMine,
|
||||||
question = content.question,
|
isEditable = pollContentState.isPollEditable,
|
||||||
answerItems = answerItems,
|
eventId = event.eventId,
|
||||||
pollKind = content.kind,
|
question = pollContentState.question,
|
||||||
isEnded = isEndedPoll,
|
answerItems = pollContentState.answerItems,
|
||||||
isEdited = content.isEdited,
|
pollKind = pollContentState.pollKind,
|
||||||
|
isEnded = pollContentState.isPollEnded,
|
||||||
|
isEdited = content.isEdited
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,13 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.model.event
|
package io.element.android.features.messages.impl.timeline.model.event
|
||||||
|
|
||||||
import io.element.android.features.poll.api.PollAnswerItem
|
import io.element.android.features.poll.api.pollcontent.PollAnswerItem
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
|
|
||||||
data class TimelineItemPollContent(
|
data class TimelineItemPollContent(
|
||||||
|
val isMine: Boolean,
|
||||||
|
val isEditable: Boolean,
|
||||||
val eventId: EventId?,
|
val eventId: EventId?,
|
||||||
val question: String,
|
val question: String,
|
||||||
val answerItems: List<PollAnswerItem>,
|
val answerItems: List<PollAnswerItem>,
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@
|
||||||
package io.element.android.features.messages.impl.timeline.model.event
|
package io.element.android.features.messages.impl.timeline.model.event
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.poll.api.PollAnswerItem
|
import io.element.android.features.poll.api.pollcontent.PollAnswerItem
|
||||||
import io.element.android.features.poll.api.aPollAnswerItemList
|
import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList
|
||||||
import io.element.android.features.poll.api.aPollQuestion
|
import io.element.android.features.poll.api.pollcontent.aPollQuestion
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
|
|
||||||
|
|
@ -28,12 +28,16 @@ open class TimelineItemPollContentProvider : PreviewParameterProvider<TimelineIt
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aTimelineItemPollContent(),
|
aTimelineItemPollContent(),
|
||||||
aTimelineItemPollContent().copy(pollKind = PollKind.Undisclosed),
|
aTimelineItemPollContent().copy(pollKind = PollKind.Undisclosed),
|
||||||
|
aTimelineItemPollContent().copy(isMine = true),
|
||||||
|
aTimelineItemPollContent().copy(isMine = true, isEditable = true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aTimelineItemPollContent(
|
fun aTimelineItemPollContent(
|
||||||
question: String = aPollQuestion(),
|
question: String = aPollQuestion(),
|
||||||
answerItems: List<PollAnswerItem> = aPollAnswerItemList(),
|
answerItems: List<PollAnswerItem> = aPollAnswerItemList(),
|
||||||
|
isMine: Boolean = false,
|
||||||
|
isEditable: Boolean = false,
|
||||||
isEnded: Boolean = false,
|
isEnded: Boolean = false,
|
||||||
isEdited: Boolean = false,
|
isEdited: Boolean = false,
|
||||||
): TimelineItemPollContent {
|
): TimelineItemPollContent {
|
||||||
|
|
@ -42,6 +46,8 @@ fun aTimelineItemPollContent(
|
||||||
pollKind = PollKind.Disclosed,
|
pollKind = PollKind.Disclosed,
|
||||||
question = question,
|
question = question,
|
||||||
answerItems = answerItems,
|
answerItems = answerItems,
|
||||||
|
isMine = isMine,
|
||||||
|
isEditable = isEditable,
|
||||||
isEnded = isEnded,
|
isEnded = isEnded,
|
||||||
isEdited = isEdited,
|
isEdited = isEdited,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.PollEnd
|
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
|
|
@ -48,6 +47,8 @@ import io.element.android.features.messages.impl.voicemessages.timeline.FakeReda
|
||||||
import io.element.android.features.messages.test.FakeMessageComposerContext
|
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||||
import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider
|
import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
|
import io.element.android.features.poll.test.actions.FakeEndPollAction
|
||||||
|
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
||||||
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
|
@ -91,7 +92,6 @@ import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
import io.element.android.tests.testutils.consumeItemsUntilTimeout
|
import io.element.android.tests.testutils.consumeItemsUntilTimeout
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import io.element.android.tests.testutils.waitForPredicate
|
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
@ -619,29 +619,6 @@ class MessagesPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - handle poll end`() = runTest {
|
|
||||||
val room = FakeMatrixRoom()
|
|
||||||
val analyticsService = FakeAnalyticsService()
|
|
||||||
val presenter = createMessagesPresenter(
|
|
||||||
matrixRoom = room,
|
|
||||||
analyticsService = analyticsService,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent()))
|
|
||||||
waitForPredicate { room.endPollInvocations.size == 1 }
|
|
||||||
cancelAndIgnoreRemainingEvents()
|
|
||||||
assertThat(room.endPollInvocations.size).isEqualTo(1)
|
|
||||||
assertThat(room.endPollInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID)
|
|
||||||
assertThat(room.endPollInvocations.first().text).isEqualTo("The poll with event id: \$anEventId has ended.")
|
|
||||||
assertThat(analyticsService.capturedEvents.size).isEqualTo(1)
|
|
||||||
assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollEnd())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle action reply to a poll`() = runTest {
|
fun `present - handle action reply to a poll`() = runTest {
|
||||||
val presenter = createMessagesPresenter()
|
val presenter = createMessagesPresenter()
|
||||||
|
|
@ -706,13 +683,14 @@ class MessagesPresenterTest {
|
||||||
dispatchers = coroutineDispatchers,
|
dispatchers = coroutineDispatchers,
|
||||||
appScope = this,
|
appScope = this,
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
analyticsService = analyticsService,
|
|
||||||
encryptionService = FakeEncryptionService(),
|
encryptionService = FakeEncryptionService(),
|
||||||
verificationService = FakeSessionVerificationService(),
|
verificationService = FakeSessionVerificationService(),
|
||||||
featureFlagService = FakeFeatureFlagService(),
|
featureFlagService = FakeFeatureFlagService(),
|
||||||
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
||||||
|
endPollAction = FakeEndPollAction(),
|
||||||
|
sendPollResponseAction = FakeSendPollResponseAction(),
|
||||||
)
|
)
|
||||||
val timelinePresenterFactory = object: TimelinePresenter.Factory {
|
val timelinePresenterFactory = object : TimelinePresenter.Factory {
|
||||||
override fun create(navigator: MessagesNavigator): TimelinePresenter {
|
override fun create(navigator: MessagesNavigator): TimelinePresenter {
|
||||||
return timelinePresenter
|
return timelinePresenter
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
|
||||||
import io.element.android.features.poll.api.aPollAnswerItemList
|
import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList
|
||||||
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
|
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
|
||||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.fixtures
|
package io.element.android.features.messages.impl.fixtures
|
||||||
|
|
||||||
|
import io.element.android.features.messages.impl.timeline.aTimelineItemDebugInfo
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||||
import io.element.android.features.messages.impl.timeline.model.InReplyToDetails
|
import io.element.android.features.messages.impl.timeline.model.InReplyToDetails
|
||||||
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
||||||
|
|
@ -32,7 +33,6 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
import io.element.android.libraries.matrix.test.room.aTimelineItemDebugInfo
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
internal fun aMessageEvent(
|
internal fun aMessageEvent(
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import io.element.android.features.messages.impl.timeline.factories.virtual.Time
|
||||||
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
|
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
|
||||||
import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
|
import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
|
||||||
import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider
|
import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider
|
||||||
|
import io.element.android.features.poll.test.pollcontent.FakePollContentStateFactory
|
||||||
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
|
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
|
||||||
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
|
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
|
||||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
||||||
|
|
@ -59,7 +60,7 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
|
||||||
),
|
),
|
||||||
redactedMessageFactory = TimelineItemContentRedactedFactory(),
|
redactedMessageFactory = TimelineItemContentRedactedFactory(),
|
||||||
stickerFactory = TimelineItemContentStickerFactory(),
|
stickerFactory = TimelineItemContentStickerFactory(),
|
||||||
pollFactory = TimelineItemContentPollFactory(matrixClient, FakeFeatureFlagService()),
|
pollFactory = TimelineItemContentPollFactory(FakeFeatureFlagService(), FakePollContentStateFactory()),
|
||||||
utdFactory = TimelineItemContentUTDFactory(),
|
utdFactory = TimelineItemContentUTDFactory(),
|
||||||
roomMembershipFactory = TimelineItemContentRoomMembershipFactory(timelineEventFormatter),
|
roomMembershipFactory = TimelineItemContentRoomMembershipFactory(timelineEventFormatter),
|
||||||
profileChangeFactory = TimelineItemContentProfileChangeFactory(timelineEventFormatter),
|
profileChangeFactory = TimelineItemContentProfileChangeFactory(timelineEventFormatter),
|
||||||
|
|
|
||||||
|
|
@ -53,5 +53,4 @@ class DefaultHtmlConverterProviderTest {
|
||||||
|
|
||||||
assertThat(htmlConverter).isNotNull()
|
assertThat(htmlConverter).isNotNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,7 @@ import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.PollEnd
|
|
||||||
import im.vector.app.features.analytics.plan.PollVote
|
|
||||||
import io.element.android.features.messages.impl.FakeMessagesNavigator
|
import io.element.android.features.messages.impl.FakeMessagesNavigator
|
||||||
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
|
||||||
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory
|
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
|
|
@ -32,8 +29,11 @@ import io.element.android.features.messages.impl.timeline.session.SessionState
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager
|
import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.aRedactedMatrixTimeline
|
import io.element.android.features.messages.impl.voicemessages.timeline.aRedactedMatrixTimeline
|
||||||
|
import io.element.android.features.poll.api.actions.EndPollAction
|
||||||
|
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||||
|
import io.element.android.features.poll.test.actions.FakeEndPollAction
|
||||||
|
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
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.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
|
||||||
|
|
@ -43,18 +43,16 @@ import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTime
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aMessageContent
|
|
||||||
import io.element.android.libraries.matrix.test.room.anEventTimelineItem
|
|
||||||
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
||||||
|
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||||
|
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
import io.element.android.tests.testutils.awaitWithLatch
|
import io.element.android.tests.testutils.awaitWithLatch
|
||||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import io.element.android.tests.testutils.waitForPredicate
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
@ -295,12 +293,10 @@ class TimelinePresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - PollAnswerSelected event calls into rust room api and analytics`() = runTest {
|
fun `present - PollAnswerSelected event`() = runTest {
|
||||||
val room = FakeMatrixRoom()
|
val sendPollResponseAction = FakeSendPollResponseAction()
|
||||||
val analyticsService = FakeAnalyticsService()
|
|
||||||
val presenter = createTimelinePresenter(
|
val presenter = createTimelinePresenter(
|
||||||
room = room,
|
sendPollResponseAction = sendPollResponseAction,
|
||||||
analyticsService = analyticsService,
|
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -309,34 +305,23 @@ class TimelinePresenterTest {
|
||||||
initialState.eventSink.invoke(TimelineEvents.PollAnswerSelected(AN_EVENT_ID, "anAnswerId"))
|
initialState.eventSink.invoke(TimelineEvents.PollAnswerSelected(AN_EVENT_ID, "anAnswerId"))
|
||||||
}
|
}
|
||||||
delay(1)
|
delay(1)
|
||||||
assertThat(room.sendPollResponseInvocations.size).isEqualTo(1)
|
sendPollResponseAction.verifyExecutionCount(1)
|
||||||
assertThat(room.sendPollResponseInvocations.first().answers).isEqualTo(listOf("anAnswerId"))
|
|
||||||
assertThat(room.sendPollResponseInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID)
|
|
||||||
assertThat(analyticsService.capturedEvents.size).isEqualTo(1)
|
|
||||||
assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollVote())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - PollEndClicked event calls into rust room api and analytics`() = runTest {
|
fun `present - PollEndClicked event`() = runTest {
|
||||||
val room = FakeMatrixRoom()
|
val endPollAction = FakeEndPollAction()
|
||||||
val analyticsService = FakeAnalyticsService()
|
|
||||||
val presenter = createTimelinePresenter(
|
val presenter = createTimelinePresenter(
|
||||||
room = room,
|
endPollAction = endPollAction,
|
||||||
analyticsService = analyticsService,
|
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(TimelineEvents.PollEndClicked(aMessageEvent().eventId!!))
|
initialState.eventSink.invoke(TimelineEvents.PollEndClicked(AN_EVENT_ID))
|
||||||
waitForPredicate { room.endPollInvocations.size == 1 }
|
|
||||||
cancelAndIgnoreRemainingEvents()
|
|
||||||
assertThat(room.endPollInvocations.size).isEqualTo(1)
|
|
||||||
assertThat(room.endPollInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID)
|
|
||||||
assertThat(room.endPollInvocations.first().text).isEqualTo("The poll with event id: \$anEventId has ended.")
|
|
||||||
assertThat(analyticsService.capturedEvents.size).isEqualTo(1)
|
|
||||||
assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollEnd())
|
|
||||||
}
|
}
|
||||||
|
delay(1)
|
||||||
|
endPollAction.verifyExecutionCount(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -379,36 +364,21 @@ class TimelinePresenterTest {
|
||||||
timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory(),
|
timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory(),
|
||||||
redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
||||||
messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
||||||
): TimelinePresenter {
|
endPollAction: EndPollAction = FakeEndPollAction(),
|
||||||
|
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
|
||||||
|
): TimelinePresenter {
|
||||||
return TimelinePresenter(
|
return TimelinePresenter(
|
||||||
timelineItemsFactory = timelineItemsFactory,
|
timelineItemsFactory = timelineItemsFactory,
|
||||||
room = FakeMatrixRoom(matrixTimeline = timeline),
|
room = FakeMatrixRoom(matrixTimeline = timeline),
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
appScope = this,
|
appScope = this,
|
||||||
navigator = messagesNavigator,
|
navigator = messagesNavigator,
|
||||||
analyticsService = FakeAnalyticsService(),
|
|
||||||
encryptionService = FakeEncryptionService(),
|
encryptionService = FakeEncryptionService(),
|
||||||
verificationService = FakeSessionVerificationService(),
|
verificationService = FakeSessionVerificationService(),
|
||||||
featureFlagService = FakeFeatureFlagService(),
|
featureFlagService = FakeFeatureFlagService(),
|
||||||
redactedVoiceMessageManager = redactedVoiceMessageManager,
|
redactedVoiceMessageManager = redactedVoiceMessageManager,
|
||||||
)
|
endPollAction = endPollAction,
|
||||||
}
|
sendPollResponseAction = sendPollResponseAction,
|
||||||
|
|
||||||
private fun TestScope.createTimelinePresenter(
|
|
||||||
room: MatrixRoom,
|
|
||||||
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
|
||||||
): TimelinePresenter {
|
|
||||||
return TimelinePresenter(
|
|
||||||
timelineItemsFactory = aTimelineItemsFactory(),
|
|
||||||
room = room,
|
|
||||||
dispatchers = testCoroutineDispatchers(),
|
|
||||||
appScope = this,
|
|
||||||
navigator = FakeMessagesNavigator(),
|
|
||||||
analyticsService = analyticsService,
|
|
||||||
encryptionService = FakeEncryptionService(),
|
|
||||||
verificationService = FakeSessionVerificationService(),
|
|
||||||
featureFlagService = FakeFeatureFlagService(),
|
|
||||||
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,298 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 com.google.common.truth.Truth.assertThat
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
|
||||||
import io.element.android.features.poll.api.PollAnswerItem
|
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
|
||||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_10
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_4
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_5
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_6
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_7
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_8
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_9
|
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.ImmutableMap
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.collections.immutable.persistentMapOf
|
|
||||||
import kotlinx.collections.immutable.toImmutableMap
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
internal class TimelineItemContentPollFactoryTest {
|
|
||||||
|
|
||||||
private val factory = TimelineItemContentPollFactory(
|
|
||||||
matrixClient = FakeMatrixClient(),
|
|
||||||
featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.Polls.key to true)),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Disclosed poll - not ended, no votes`() = runTest {
|
|
||||||
assertThat(factory.create(aPollContent(), eventId = null)).isEqualTo(aTimelineItemPollContent())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Disclosed poll - not ended, some votes, including one from current user`() = runTest {
|
|
||||||
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
|
||||||
assertThat(
|
|
||||||
factory.create(aPollContent(votes = votes), eventId = null)
|
|
||||||
)
|
|
||||||
.isEqualTo(
|
|
||||||
aTimelineItemPollContent(
|
|
||||||
answerItems = listOf(
|
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_1, votesCount = 3, percentage = 0.3f),
|
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, votesCount = 6, percentage = 0.6f),
|
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_3),
|
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_4, votesCount = 1, percentage = 0.1f),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Disclosed poll - ended, no votes, no winner`() = runTest {
|
|
||||||
assertThat(
|
|
||||||
factory.create(aPollContent(endTime = 1UL), eventId = null)
|
|
||||||
).isEqualTo(
|
|
||||||
aTimelineItemPollContent().let {
|
|
||||||
it.copy(
|
|
||||||
answerItems = it.answerItems.map { answerItem -> answerItem.copy(isEnabled = false) },
|
|
||||||
isEnded = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Disclosed poll - ended, some votes, including one from current user (winner)`() = runTest {
|
|
||||||
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
|
||||||
assertThat(
|
|
||||||
factory.create(aPollContent(votes = votes, endTime = 1UL), eventId = null)
|
|
||||||
)
|
|
||||||
.isEqualTo(
|
|
||||||
aTimelineItemPollContent(
|
|
||||||
answerItems = listOf(
|
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, votesCount = 3, percentage = 0.3f),
|
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, isWinner = true, votesCount = 6, percentage = 0.6f),
|
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false),
|
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, votesCount = 1, percentage = 0.1f),
|
|
||||||
),
|
|
||||||
isEnded = true,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Disclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest {
|
|
||||||
val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
|
||||||
assertThat(
|
|
||||||
factory.create(aPollContent(votes = votes, endTime = 1UL), eventId = null)
|
|
||||||
)
|
|
||||||
.isEqualTo(
|
|
||||||
aTimelineItemPollContent(
|
|
||||||
answerItems = listOf(
|
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f),
|
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, votesCount = 2, percentage = 0.2f),
|
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false),
|
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f),
|
|
||||||
),
|
|
||||||
isEnded = true,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Undisclosed poll - not ended, no votes`() = runTest {
|
|
||||||
assertThat(
|
|
||||||
factory.create(aPollContent(PollKind.Undisclosed).copy(), eventId = null)
|
|
||||||
).isEqualTo(
|
|
||||||
aTimelineItemPollContent(pollKind = PollKind.Undisclosed).let {
|
|
||||||
it.copy(answerItems = it.answerItems.map { answerItem -> answerItem.copy(isDisclosed = false) })
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Undisclosed poll - not ended, some votes, including one from current user`() = runTest {
|
|
||||||
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
|
||||||
assertThat(
|
|
||||||
factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes), eventId = null)
|
|
||||||
)
|
|
||||||
.isEqualTo(
|
|
||||||
aTimelineItemPollContent(
|
|
||||||
pollKind = PollKind.Undisclosed,
|
|
||||||
answerItems = listOf(
|
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_1, isDisclosed = false, votesCount = 3, percentage = 0.3f),
|
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_2, isDisclosed = false, isSelected = true, votesCount = 6, percentage = 0.6f),
|
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_3, isDisclosed = false),
|
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_4, isDisclosed = false, votesCount = 1, percentage = 0.1f),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Undisclosed poll - ended, no votes, no winner`() = runTest {
|
|
||||||
assertThat(
|
|
||||||
factory.create(aPollContent(pollKind = PollKind.Undisclosed, endTime = 1UL), eventId = null)
|
|
||||||
).isEqualTo(
|
|
||||||
aTimelineItemPollContent().let {
|
|
||||||
it.copy(
|
|
||||||
pollKind = PollKind.Undisclosed,
|
|
||||||
answerItems = it.answerItems.map { answerItem ->
|
|
||||||
answerItem.copy(isDisclosed = true, isEnabled = false, isWinner = false)
|
|
||||||
},
|
|
||||||
isEnded = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Undisclosed poll - ended, some votes, including one from current user (winner)`() = runTest {
|
|
||||||
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
|
||||||
assertThat(
|
|
||||||
factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes, endTime = 1UL), eventId = null)
|
|
||||||
)
|
|
||||||
.isEqualTo(
|
|
||||||
aTimelineItemPollContent(
|
|
||||||
pollKind = PollKind.Undisclosed,
|
|
||||||
answerItems = listOf(
|
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, votesCount = 3, percentage = 0.3f),
|
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, isWinner = true, votesCount = 6, percentage = 0.6f),
|
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false),
|
|
||||||
aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, votesCount = 1, percentage = 0.1f),
|
|
||||||
),
|
|
||||||
isEnded = true,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Undisclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest {
|
|
||||||
val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
|
||||||
assertThat(
|
|
||||||
factory.create(aPollContent(PollKind.Undisclosed).copy(votes = votes, endTime = 1UL), eventId = null)
|
|
||||||
)
|
|
||||||
.isEqualTo(
|
|
||||||
aTimelineItemPollContent(
|
|
||||||
pollKind = PollKind.Undisclosed,
|
|
||||||
answerItems = listOf(
|
|
||||||
aPollAnswerItem(A_POLL_ANSWER_1, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f),
|
|
||||||
aPollAnswerItem(A_POLL_ANSWER_2, isSelected = true, isEnabled = false, votesCount = 2, percentage = 0.2f),
|
|
||||||
aPollAnswerItem(A_POLL_ANSWER_3, isEnabled = false),
|
|
||||||
aPollAnswerItem(A_POLL_ANSWER_4, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f),
|
|
||||||
),
|
|
||||||
isEnded = true,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `eventId is populated`() = runTest {
|
|
||||||
assertThat(factory.create(aPollContent(), eventId = null))
|
|
||||||
.isEqualTo(aTimelineItemPollContent(eventId = null))
|
|
||||||
|
|
||||||
assertThat(factory.create(aPollContent(), eventId = AN_EVENT_ID))
|
|
||||||
.isEqualTo(aTimelineItemPollContent(eventId = AN_EVENT_ID))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun aPollContent(
|
|
||||||
pollKind: PollKind = PollKind.Disclosed,
|
|
||||||
votes: ImmutableMap<String, ImmutableList<UserId>> = persistentMapOf(),
|
|
||||||
endTime: ULong? = null,
|
|
||||||
): PollContent = PollContent(
|
|
||||||
question = A_POLL_QUESTION,
|
|
||||||
kind = pollKind,
|
|
||||||
maxSelections = 1UL,
|
|
||||||
answers = persistentListOf(A_POLL_ANSWER_1, A_POLL_ANSWER_2, A_POLL_ANSWER_3, A_POLL_ANSWER_4),
|
|
||||||
votes = votes,
|
|
||||||
endTime = endTime,
|
|
||||||
isEdited = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun aTimelineItemPollContent(
|
|
||||||
eventId: EventId? = null,
|
|
||||||
pollKind: PollKind = PollKind.Disclosed,
|
|
||||||
answerItems: List<PollAnswerItem> = listOf(
|
|
||||||
aPollAnswerItem(A_POLL_ANSWER_1),
|
|
||||||
aPollAnswerItem(A_POLL_ANSWER_2),
|
|
||||||
aPollAnswerItem(A_POLL_ANSWER_3),
|
|
||||||
aPollAnswerItem(A_POLL_ANSWER_4),
|
|
||||||
),
|
|
||||||
isEnded: Boolean = false,
|
|
||||||
) = TimelineItemPollContent(
|
|
||||||
eventId = eventId,
|
|
||||||
question = A_POLL_QUESTION,
|
|
||||||
answerItems = answerItems,
|
|
||||||
pollKind = pollKind,
|
|
||||||
isEnded = isEnded,
|
|
||||||
isEdited = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun aPollAnswerItem(
|
|
||||||
answer: PollAnswer,
|
|
||||||
isSelected: Boolean = false,
|
|
||||||
isEnabled: Boolean = true,
|
|
||||||
isWinner: Boolean = false,
|
|
||||||
isDisclosed: Boolean = true,
|
|
||||||
votesCount: Int = 0,
|
|
||||||
percentage: Float = 0f,
|
|
||||||
) = PollAnswerItem(
|
|
||||||
answer = answer,
|
|
||||||
isSelected = isSelected,
|
|
||||||
isEnabled = isEnabled,
|
|
||||||
isWinner = isWinner,
|
|
||||||
isDisclosed = isDisclosed,
|
|
||||||
votesCount = votesCount,
|
|
||||||
percentage = percentage,
|
|
||||||
)
|
|
||||||
|
|
||||||
private companion object TestData {
|
|
||||||
private const val A_POLL_QUESTION = "What is your favorite food?"
|
|
||||||
private val A_POLL_ANSWER_1 = PollAnswer("id_1", "Pizza")
|
|
||||||
private val A_POLL_ANSWER_2 = PollAnswer("id_2", "Pasta")
|
|
||||||
private val A_POLL_ANSWER_3 = PollAnswer("id_3", "French Fries")
|
|
||||||
private val A_POLL_ANSWER_4 = PollAnswer("id_4", "Hamburger")
|
|
||||||
|
|
||||||
private val MY_USER_WINNING_VOTES = persistentMapOf(
|
|
||||||
A_POLL_ANSWER_1 to persistentListOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4),
|
|
||||||
A_POLL_ANSWER_2 to persistentListOf(A_USER_ID /* my vote */, A_USER_ID_5, A_USER_ID_6, A_USER_ID_7, A_USER_ID_8, A_USER_ID_9), // winner
|
|
||||||
A_POLL_ANSWER_3 to persistentListOf(),
|
|
||||||
A_POLL_ANSWER_4 to persistentListOf(A_USER_ID_10),
|
|
||||||
)
|
|
||||||
private val OTHER_WINNING_VOTES = persistentMapOf(
|
|
||||||
A_POLL_ANSWER_1 to persistentListOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4, A_USER_ID_5), // winner
|
|
||||||
A_POLL_ANSWER_2 to persistentListOf(A_USER_ID /* my vote */, A_USER_ID_6),
|
|
||||||
A_POLL_ANSWER_3 to persistentListOf(),
|
|
||||||
A_POLL_ANSWER_4 to persistentListOf(A_USER_ID_7, A_USER_ID_8, A_USER_ID_9, A_USER_ID_10), // winner
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.groups
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
||||||
|
import io.element.android.features.messages.impl.timeline.aTimelineItemDebugInfo
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||||
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
|
@ -28,7 +29,6 @@ import io.element.android.libraries.designsystem.components.avatar.anAvatarData
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.room.aTimelineItemDebugInfo
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageT
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.media.aMediaSource
|
import io.element.android.libraries.matrix.test.media.aMediaSource
|
||||||
import io.element.android.libraries.matrix.test.room.aMessageContent
|
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||||
import io.element.android.libraries.matrix.test.room.aPollContent
|
import io.element.android.libraries.matrix.test.timeline.aPollContent
|
||||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
||||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
|
||||||
|
|
@ -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.api.actions
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
||||||
|
interface EndPollAction {
|
||||||
|
suspend fun execute(pollStartId: EventId): Result<Unit>
|
||||||
|
}
|
||||||
|
|
@ -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.api.actions
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
||||||
|
interface SendPollResponseAction {
|
||||||
|
suspend fun execute(pollStartId: EventId, answerId: String): Result<Unit>
|
||||||
|
}
|
||||||
|
|
@ -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.features.poll.api.history
|
||||||
|
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
|
|
||||||
|
interface PollHistoryEntryPoint : FeatureEntryPoint {
|
||||||
|
fun createNode(parentNode: Node, buildContext: BuildContext): Node
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.poll.api
|
package io.element.android.features.poll.api.pollcontent
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.poll.api
|
package io.element.android.features.poll.api.pollcontent
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* 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.pollcontent
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI model for a PollContent.
|
||||||
|
* @property eventId the event id of the poll.
|
||||||
|
* @property question the poll question.
|
||||||
|
* @property answerItems the list of answers.
|
||||||
|
* @property pollKind the kind of poll.
|
||||||
|
* @property isPollEditable whether the poll is editable.
|
||||||
|
* @property isPollEnded whether the poll is ended.
|
||||||
|
* @property isMine whether the poll has been created by me.
|
||||||
|
*/
|
||||||
|
data class PollContentState(
|
||||||
|
val eventId: EventId?,
|
||||||
|
val question: String,
|
||||||
|
val answerItems: ImmutableList<PollAnswerItem>,
|
||||||
|
val pollKind: PollKind,
|
||||||
|
val isPollEditable: Boolean,
|
||||||
|
val isPollEnded: Boolean,
|
||||||
|
val isMine: Boolean,
|
||||||
|
)
|
||||||
|
|
@ -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.features.poll.api.pollcontent
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||||
|
|
||||||
|
interface PollContentStateFactory {
|
||||||
|
suspend fun create(event: EventTimelineItem, content: PollContent): PollContentState
|
||||||
|
}
|
||||||
|
|
@ -14,9 +14,11 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.poll.api
|
package io.element.android.features.poll.api.pollcontent
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||||
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
fun aPollQuestion() = "What type of food should we have at the party?"
|
fun aPollQuestion() = "What type of food should we have at the party?"
|
||||||
|
|
@ -79,3 +81,25 @@ fun aPollAnswerItem(
|
||||||
votesCount = votesCount,
|
votesCount = votesCount,
|
||||||
percentage = percentage
|
percentage = percentage
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun aPollContentState(
|
||||||
|
isMine: Boolean = false,
|
||||||
|
isEnded: Boolean = false,
|
||||||
|
isDisclosed: Boolean = true,
|
||||||
|
hasVotes: Boolean = true,
|
||||||
|
question: String = aPollQuestion(),
|
||||||
|
pollKind: PollKind = PollKind.Disclosed,
|
||||||
|
answerItems: ImmutableList<PollAnswerItem> = aPollAnswerItemList(
|
||||||
|
isEnded = isEnded,
|
||||||
|
isDisclosed = isDisclosed,
|
||||||
|
hasVotes = hasVotes
|
||||||
|
),
|
||||||
|
) = PollContentState(
|
||||||
|
eventId = null,
|
||||||
|
question = question,
|
||||||
|
answerItems = answerItems,
|
||||||
|
pollKind = pollKind,
|
||||||
|
isPollEditable = isMine && !isEnded,
|
||||||
|
isPollEnded = isEnded,
|
||||||
|
isMine = isMine,
|
||||||
|
)
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.poll.api
|
package io.element.android.features.poll.api.pollcontent
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -49,6 +49,29 @@ import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PollContentView(
|
||||||
|
state: PollContentState,
|
||||||
|
onAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit,
|
||||||
|
onPollEdit: (pollStartId: EventId) -> Unit,
|
||||||
|
onPollEnd: (pollStartId: EventId) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
PollContentView(
|
||||||
|
eventId = state.eventId,
|
||||||
|
question = state.question,
|
||||||
|
answerItems = state.answerItems,
|
||||||
|
pollKind = state.pollKind,
|
||||||
|
isPollEditable = state.isPollEditable,
|
||||||
|
isPollEnded = state.isPollEnded,
|
||||||
|
isMine = state.isMine,
|
||||||
|
onPollEdit = onPollEdit,
|
||||||
|
onAnswerSelected = onAnswerSelected,
|
||||||
|
onPollEnd = onPollEnd,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PollContentView(
|
fun PollContentView(
|
||||||
eventId: EventId?,
|
eventId: EventId?,
|
||||||
|
|
@ -40,6 +40,7 @@ dependencies {
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(projects.features.messages.api)
|
implementation(projects.features.messages.api)
|
||||||
|
implementation(projects.libraries.dateformatter.api)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
|
|
@ -51,6 +52,8 @@ dependencies {
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.features.messages.test)
|
testImplementation(projects.features.messages.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
testImplementation(projects.libraries.dateformatter.test)
|
||||||
|
testImplementation(projects.features.poll.test)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* 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 com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.poll.api.history.PollHistoryEntryPoint
|
||||||
|
import io.element.android.libraries.architecture.createNode
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultPollHistoryEntryPoint @Inject constructor() : PollHistoryEntryPoint {
|
||||||
|
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
||||||
|
return parentNode.createNode<PollHistoryFlowNode>(buildContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* 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.impl.history.model.PollHistoryFilter
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
||||||
|
sealed interface PollHistoryEvents {
|
||||||
|
data object LoadMore : PollHistoryEvents
|
||||||
|
data class PollAnswerSelected(val pollStartId: EventId, val answerId: String) : PollHistoryEvents
|
||||||
|
data class PollEndClicked(val pollStartId: EventId) : PollHistoryEvents
|
||||||
|
data class OnFilterSelected(val filter: PollHistoryFilter) : PollHistoryEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* 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 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 com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.poll.api.create.CreatePollEntryPoint
|
||||||
|
import io.element.android.features.poll.api.create.CreatePollMode
|
||||||
|
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.RoomScope
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@ContributesNode(RoomScope::class)
|
||||||
|
class PollHistoryFlowNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val createPollEntryPoint: CreatePollEntryPoint,
|
||||||
|
) : BackstackNode<PollHistoryFlowNode.NavTarget>(
|
||||||
|
backstack = BackStack(
|
||||||
|
initialElement = NavTarget.Root,
|
||||||
|
savedStateMap = buildContext.savedStateMap,
|
||||||
|
),
|
||||||
|
buildContext = buildContext,
|
||||||
|
plugins = plugins
|
||||||
|
) {
|
||||||
|
|
||||||
|
sealed interface NavTarget : Parcelable {
|
||||||
|
@Parcelize
|
||||||
|
data object Root : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class EditPoll(val pollStartEventId: EventId) : NavTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
return when (navTarget) {
|
||||||
|
is NavTarget.EditPoll -> {
|
||||||
|
createPollEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
.params(CreatePollEntryPoint.Params(mode = CreatePollMode.EditPoll(eventId = navTarget.pollStartEventId)))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
NavTarget.Root -> {
|
||||||
|
val callback = object : PollHistoryNode.Callback {
|
||||||
|
override fun onEditPoll(pollStartEventId: EventId) {
|
||||||
|
backstack.push(NavTarget.EditPoll(pollStartEventId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<PollHistoryNode>(
|
||||||
|
buildContext = buildContext,
|
||||||
|
plugins = listOf(callback)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
Children(
|
||||||
|
navModel = backstack,
|
||||||
|
modifier = modifier,
|
||||||
|
transitionHandler = rememberDefaultTransitionHandler()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* 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 androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
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.core.plugin.plugins
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
||||||
|
@ContributesNode(RoomScope::class)
|
||||||
|
class PollHistoryNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val presenter: PollHistoryPresenter,
|
||||||
|
) : Node(
|
||||||
|
buildContext = buildContext,
|
||||||
|
plugins = plugins,
|
||||||
|
) {
|
||||||
|
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun onEditPoll(pollStartEventId: EventId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onEditPoll(pollStartEventId: EventId) {
|
||||||
|
plugins<Callback>().forEach { it.onEditPoll(pollStartEventId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
PollHistoryView(
|
||||||
|
state = presenter.present(),
|
||||||
|
modifier = modifier,
|
||||||
|
onEditPoll = ::onEditPoll,
|
||||||
|
goBack = this::navigateUp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* 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 androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import io.element.android.features.poll.api.actions.EndPollAction
|
||||||
|
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||||
|
import io.element.android.features.poll.impl.history.model.PollHistoryFilter
|
||||||
|
import io.element.android.features.poll.impl.history.model.PollHistoryItems
|
||||||
|
import io.element.android.features.poll.impl.history.model.PollHistoryItemsFactory
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class PollHistoryPresenter @Inject constructor(
|
||||||
|
private val room: MatrixRoom,
|
||||||
|
private val appCoroutineScope: CoroutineScope,
|
||||||
|
private val sendPollResponseAction: SendPollResponseAction,
|
||||||
|
private val endPollAction: EndPollAction,
|
||||||
|
private val pollHistoryItemFactory: PollHistoryItemsFactory,
|
||||||
|
) : Presenter<PollHistoryState> {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): PollHistoryState {
|
||||||
|
// TODO use room.rememberPollHistory() when working properly?
|
||||||
|
val timeline = room.timeline
|
||||||
|
val paginationState by timeline.paginationState.collectAsState()
|
||||||
|
val pollHistoryItemsFlow = remember {
|
||||||
|
timeline.timelineItems.map { items ->
|
||||||
|
pollHistoryItemFactory.create(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var activeFilter by rememberSaveable {
|
||||||
|
mutableStateOf(PollHistoryFilter.ONGOING)
|
||||||
|
}
|
||||||
|
val pollHistoryItems by pollHistoryItemsFlow.collectAsState(initial = PollHistoryItems())
|
||||||
|
LaunchedEffect(paginationState, pollHistoryItems.size) {
|
||||||
|
if (pollHistoryItems.size == 0 && paginationState.canBackPaginate) loadMore(timeline)
|
||||||
|
}
|
||||||
|
val isLoading by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
pollHistoryItems.size == 0 || paginationState.isBackPaginating
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
fun handleEvents(event: PollHistoryEvents) {
|
||||||
|
when (event) {
|
||||||
|
is PollHistoryEvents.LoadMore -> {
|
||||||
|
coroutineScope.loadMore(timeline)
|
||||||
|
}
|
||||||
|
is PollHistoryEvents.PollAnswerSelected -> appCoroutineScope.launch {
|
||||||
|
sendPollResponseAction.execute(pollStartId = event.pollStartId, answerId = event.answerId)
|
||||||
|
}
|
||||||
|
is PollHistoryEvents.PollEndClicked -> appCoroutineScope.launch {
|
||||||
|
endPollAction.execute(pollStartId = event.pollStartId)
|
||||||
|
}
|
||||||
|
is PollHistoryEvents.OnFilterSelected -> {
|
||||||
|
activeFilter = event.filter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return PollHistoryState(
|
||||||
|
isLoading = isLoading,
|
||||||
|
hasMoreToLoad = paginationState.hasMoreToLoadBackwards,
|
||||||
|
pollHistoryItems = pollHistoryItems,
|
||||||
|
activeFilter = activeFilter,
|
||||||
|
eventSink = ::handleEvents,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.loadMore(pollHistory: MatrixTimeline) = launch {
|
||||||
|
pollHistory.paginateBackwards(200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* 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.impl.history.model.PollHistoryFilter
|
||||||
|
import io.element.android.features.poll.impl.history.model.PollHistoryItem
|
||||||
|
import io.element.android.features.poll.impl.history.model.PollHistoryItems
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
data class PollHistoryState(
|
||||||
|
val isLoading: Boolean,
|
||||||
|
val hasMoreToLoad: Boolean,
|
||||||
|
val activeFilter: PollHistoryFilter,
|
||||||
|
val pollHistoryItems: PollHistoryItems,
|
||||||
|
val eventSink: (PollHistoryEvents) -> Unit,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun pollHistoryForFilter(filter: PollHistoryFilter): ImmutableList<PollHistoryItem> {
|
||||||
|
return when (filter) {
|
||||||
|
PollHistoryFilter.ONGOING -> pollHistoryItems.ongoing
|
||||||
|
PollHistoryFilter.PAST -> pollHistoryItems.past
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* 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 androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.poll.api.pollcontent.PollContentState
|
||||||
|
import io.element.android.features.poll.api.pollcontent.aPollContentState
|
||||||
|
import io.element.android.features.poll.impl.history.model.PollHistoryFilter
|
||||||
|
import io.element.android.features.poll.impl.history.model.PollHistoryItem
|
||||||
|
import io.element.android.features.poll.impl.history.model.PollHistoryItems
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
class PollHistoryStateProvider : PreviewParameterProvider<PollHistoryState> {
|
||||||
|
override val values: Sequence<PollHistoryState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aPollHistoryState(
|
||||||
|
isLoading = false,
|
||||||
|
hasMoreToLoad = false,
|
||||||
|
activeFilter = PollHistoryFilter.ONGOING,
|
||||||
|
),
|
||||||
|
aPollHistoryState(
|
||||||
|
isLoading = true,
|
||||||
|
hasMoreToLoad = true,
|
||||||
|
activeFilter = PollHistoryFilter.PAST,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aPollHistoryState(
|
||||||
|
isLoading: Boolean = false,
|
||||||
|
hasMoreToLoad: Boolean = false,
|
||||||
|
activeFilter: PollHistoryFilter = PollHistoryFilter.ONGOING,
|
||||||
|
currentItems: ImmutableList<PollHistoryItem> = persistentListOf(
|
||||||
|
aPollHistoryItem(),
|
||||||
|
),
|
||||||
|
) = PollHistoryState(
|
||||||
|
isLoading = isLoading,
|
||||||
|
hasMoreToLoad = hasMoreToLoad,
|
||||||
|
activeFilter = activeFilter,
|
||||||
|
pollHistoryItems = PollHistoryItems(
|
||||||
|
ongoing = currentItems,
|
||||||
|
past = currentItems,
|
||||||
|
),
|
||||||
|
eventSink = {},
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun aPollHistoryItem(
|
||||||
|
formattedDate: String = "01/12/2023",
|
||||||
|
state: PollContentState = aPollContentState(),
|
||||||
|
) = PollHistoryItem(
|
||||||
|
formattedDate = formattedDate,
|
||||||
|
state = state,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,257 @@
|
||||||
|
/*
|
||||||
|
* 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 androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
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.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
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.pollcontent.PollContentView
|
||||||
|
import io.element.android.features.poll.impl.R
|
||||||
|
import io.element.android.features.poll.impl.history.model.PollHistoryFilter
|
||||||
|
import io.element.android.features.poll.impl.history.model.PollHistoryItem
|
||||||
|
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.Button
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SegmentedButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||||
|
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.core.EventId
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun PollHistoryView(
|
||||||
|
state: PollHistoryState,
|
||||||
|
onEditPoll: (EventId) -> Unit,
|
||||||
|
goBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun onLoadMore() {
|
||||||
|
state.eventSink(PollHistoryEvents.LoadMore)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAnswerSelected(pollStartId: EventId, answerId: String) {
|
||||||
|
state.eventSink(PollHistoryEvents.PollAnswerSelected(pollStartId, answerId))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPollEnd(pollStartId: EventId) {
|
||||||
|
state.eventSink(PollHistoryEvents.PollEndClicked(pollStartId))
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.screen_polls_history_title),
|
||||||
|
style = ElementTheme.typography.aliasScreenTitle,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
BackButton(onClick = goBack)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
|
) {
|
||||||
|
val pagerState = rememberPagerState(state.activeFilter.ordinal, 0f) {
|
||||||
|
PollHistoryFilter.entries.size
|
||||||
|
}
|
||||||
|
LaunchedEffect(state.activeFilter) {
|
||||||
|
pagerState.scrollToPage(state.activeFilter.ordinal)
|
||||||
|
}
|
||||||
|
PollHistoryFilterButtons(
|
||||||
|
activeFilter = state.activeFilter,
|
||||||
|
onFilterSelected = { state.eventSink(PollHistoryEvents.OnFilterSelected(it)) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
)
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
userScrollEnabled = false,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) { page ->
|
||||||
|
val filter = PollHistoryFilter.entries[page]
|
||||||
|
val pollHistoryItems = state.pollHistoryForFilter(filter)
|
||||||
|
PollHistoryList(
|
||||||
|
filter = filter,
|
||||||
|
pollHistoryItems = pollHistoryItems,
|
||||||
|
hasMoreToLoad = state.hasMoreToLoad,
|
||||||
|
isLoading = state.isLoading,
|
||||||
|
onAnswerSelected = ::onAnswerSelected,
|
||||||
|
onPollEdit = onEditPoll,
|
||||||
|
onPollEnd = ::onPollEnd,
|
||||||
|
onLoadMore = ::onLoadMore,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun PollHistoryFilterButtons(
|
||||||
|
activeFilter: PollHistoryFilter,
|
||||||
|
onFilterSelected: (PollHistoryFilter) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
SingleChoiceSegmentedButtonRow(modifier = modifier) {
|
||||||
|
PollHistoryFilter.entries.forEach { filter ->
|
||||||
|
SegmentedButton(
|
||||||
|
index = filter.ordinal,
|
||||||
|
count = PollHistoryFilter.entries.size,
|
||||||
|
selected = activeFilter == filter,
|
||||||
|
onClick = { onFilterSelected(filter) },
|
||||||
|
text = stringResource(filter.stringResource),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PollHistoryList(
|
||||||
|
filter: PollHistoryFilter,
|
||||||
|
pollHistoryItems: ImmutableList<PollHistoryItem>,
|
||||||
|
hasMoreToLoad: Boolean,
|
||||||
|
isLoading: Boolean,
|
||||||
|
onAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit,
|
||||||
|
onPollEdit: (pollStartId: EventId) -> Unit,
|
||||||
|
onPollEnd: (pollStartId: EventId) -> Unit,
|
||||||
|
onLoadMore: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
LazyColumn(
|
||||||
|
state = lazyListState,
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
items(pollHistoryItems) { pollHistoryItem ->
|
||||||
|
PollHistoryItemRow(
|
||||||
|
pollHistoryItem = pollHistoryItem,
|
||||||
|
onAnswerSelected = onAnswerSelected,
|
||||||
|
onPollEdit = onPollEdit,
|
||||||
|
onPollEnd = onPollEnd,
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (pollHistoryItems.isEmpty()) {
|
||||||
|
item {
|
||||||
|
val emptyStringResource = if (filter == PollHistoryFilter.PAST) {
|
||||||
|
stringResource(R.string.screen_polls_history_empty_past)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.screen_polls_history_empty_ongoing)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = emptyStringResource,
|
||||||
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasMoreToLoad) {
|
||||||
|
item {
|
||||||
|
Button(
|
||||||
|
text = stringResource(CommonStrings.action_load_more),
|
||||||
|
showProgress = isLoading,
|
||||||
|
onClick = onLoadMore,
|
||||||
|
modifier = Modifier.padding(vertical = 24.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PollHistoryItemRow(
|
||||||
|
pollHistoryItem: PollHistoryItem,
|
||||||
|
onAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit,
|
||||||
|
onPollEdit: (pollStartId: EventId) -> Unit,
|
||||||
|
onPollEnd: (pollStartId: EventId) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier,
|
||||||
|
border = BorderStroke(1.dp, ElementTheme.colors.borderInteractiveSecondary),
|
||||||
|
shape = RoundedCornerShape(size = 12.dp)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
|
Text(
|
||||||
|
text = pollHistoryItem.formattedDate,
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
PollContentView(
|
||||||
|
state = pollHistoryItem.state,
|
||||||
|
onAnswerSelected = onAnswerSelected,
|
||||||
|
onPollEdit = onPollEdit,
|
||||||
|
onPollEnd = onPollEnd,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun PollHistoryViewPreview(
|
||||||
|
@PreviewParameter(PollHistoryStateProvider::class) state: PollHistoryState
|
||||||
|
) = ElementPreview {
|
||||||
|
PollHistoryView(
|
||||||
|
state = state,
|
||||||
|
onEditPoll = {},
|
||||||
|
goBack = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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.features.poll.impl.history.model
|
||||||
|
|
||||||
|
import io.element.android.features.poll.impl.R
|
||||||
|
|
||||||
|
enum class PollHistoryFilter(val stringResource: Int) {
|
||||||
|
ONGOING(R.string.screen_polls_history_filter_ongoing),
|
||||||
|
PAST(R.string.screen_polls_history_filter_past),
|
||||||
|
}
|
||||||
|
|
@ -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.features.poll.impl.history.model
|
||||||
|
|
||||||
|
import io.element.android.features.poll.api.pollcontent.PollContentState
|
||||||
|
|
||||||
|
data class PollHistoryItem(
|
||||||
|
val formattedDate: String,
|
||||||
|
val state: PollContentState,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* 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.model
|
||||||
|
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
data class PollHistoryItems(
|
||||||
|
val ongoing: ImmutableList<PollHistoryItem> = persistentListOf(),
|
||||||
|
val past: ImmutableList<PollHistoryItem> = persistentListOf(),
|
||||||
|
) {
|
||||||
|
val size = ongoing.size + past.size
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* 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.model
|
||||||
|
|
||||||
|
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.collections.immutable.toPersistentList
|
||||||
|
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>): PollHistoryItems = withContext(dispatchers.computation) {
|
||||||
|
val past = ArrayList<PollHistoryItem>()
|
||||||
|
val ongoing = ArrayList<PollHistoryItem>()
|
||||||
|
for (index in timelineItems.indices.reversed()) {
|
||||||
|
val timelineItem = timelineItems[index]
|
||||||
|
val pollHistoryItem = create(timelineItem) ?: continue
|
||||||
|
if (pollHistoryItem.state.isPollEnded) {
|
||||||
|
past.add(pollHistoryItem)
|
||||||
|
} else {
|
||||||
|
ongoing.add(pollHistoryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PollHistoryItems(
|
||||||
|
ongoing = ongoing.toPersistentList(),
|
||||||
|
past = past.toPersistentList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
formattedDate = daySeparatorFormatter.format(timelineItem.event.timestamp),
|
||||||
|
state = pollContentState
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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,
|
||||||
|
question = content.question,
|
||||||
|
answerItems = answerItems.toImmutableList(),
|
||||||
|
pollKind = content.kind,
|
||||||
|
isPollEditable = event.isEditable,
|
||||||
|
isPollEnded = isPollEnded,
|
||||||
|
isMine = event.isOwn,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,4 +11,9 @@
|
||||||
<string name="screen_edit_poll_delete_confirmation">"Are you sure you want to delete this poll?"</string>
|
<string name="screen_edit_poll_delete_confirmation">"Are you sure you want to delete this poll?"</string>
|
||||||
<string name="screen_edit_poll_delete_confirmation_title">"Delete Poll"</string>
|
<string name="screen_edit_poll_delete_confirmation_title">"Delete Poll"</string>
|
||||||
<string name="screen_edit_poll_title">"Edit poll"</string>
|
<string name="screen_edit_poll_title">"Edit poll"</string>
|
||||||
|
<string name="screen_polls_history_empty_ongoing">"Can\'t find any ongoing polls."</string>
|
||||||
|
<string name="screen_polls_history_empty_past">"Can\'t find any past polls."</string>
|
||||||
|
<string name="screen_polls_history_filter_ongoing">"Ongoing"</string>
|
||||||
|
<string name="screen_polls_history_filter_past">"Past"</string>
|
||||||
|
<string name="screen_polls_history_title">"Polls"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* 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 io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||||
|
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
||||||
|
import io.element.android.libraries.matrix.test.timeline.aPollContent
|
||||||
|
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
fun aPollTimeline(
|
||||||
|
polls: Map<EventId, PollContent> = emptyMap(),
|
||||||
|
): FakeMatrixTimeline {
|
||||||
|
return FakeMatrixTimeline(
|
||||||
|
initialTimelineItems = polls.map { entry ->
|
||||||
|
MatrixTimelineItem.Event(
|
||||||
|
entry.key.hashCode().toLong(),
|
||||||
|
anEventTimelineItem(
|
||||||
|
eventId = entry.key,
|
||||||
|
content = entry.value,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun anOngoingPollContent() = aPollContent(
|
||||||
|
question = "Do you like polls?",
|
||||||
|
answers = persistentListOf(
|
||||||
|
PollAnswer("1", "Yes"),
|
||||||
|
PollAnswer("2", "No"),
|
||||||
|
PollAnswer("2", "Maybe"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun anEndedPollContent() = anOngoingPollContent().copy(
|
||||||
|
endTime = 1702400215U
|
||||||
|
)
|
||||||
|
|
@ -25,21 +25,17 @@ import im.vector.app.features.analytics.plan.Composer
|
||||||
import im.vector.app.features.analytics.plan.PollCreation
|
import im.vector.app.features.analytics.plan.PollCreation
|
||||||
import io.element.android.features.messages.test.FakeMessageComposerContext
|
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||||
import io.element.android.features.poll.api.create.CreatePollMode
|
import io.element.android.features.poll.api.create.CreatePollMode
|
||||||
|
import io.element.android.features.poll.impl.aPollTimeline
|
||||||
|
import io.element.android.features.poll.impl.anOngoingPollContent
|
||||||
import io.element.android.features.poll.impl.data.PollRepository
|
import io.element.android.features.poll.impl.data.PollRepository
|
||||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
|
||||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.room.SavePollInvocation
|
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aPollContent
|
import io.element.android.libraries.matrix.test.room.SavePollInvocation
|
||||||
import io.element.android.libraries.matrix.test.room.anEventTimelineItem
|
|
||||||
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -52,8 +48,12 @@ class CreatePollPresenterTest {
|
||||||
|
|
||||||
private val pollEventId = AN_EVENT_ID
|
private val pollEventId = AN_EVENT_ID
|
||||||
private var navUpInvocationsCount = 0
|
private var navUpInvocationsCount = 0
|
||||||
private val existingPoll = anExistingPoll()
|
private val existingPoll = anOngoingPollContent()
|
||||||
private val fakeMatrixRoom = createFakeMatrixRoom(existingPoll)
|
private val fakeMatrixRoom = FakeMatrixRoom(
|
||||||
|
matrixTimeline = aPollTimeline(
|
||||||
|
mapOf(pollEventId to existingPoll)
|
||||||
|
)
|
||||||
|
)
|
||||||
private val fakeAnalyticsService = FakeAnalyticsService()
|
private val fakeAnalyticsService = FakeAnalyticsService()
|
||||||
private val fakeMessageComposerContext = FakeMessageComposerContext()
|
private val fakeMessageComposerContext = FakeMessageComposerContext()
|
||||||
|
|
||||||
|
|
@ -80,7 +80,9 @@ class CreatePollPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `in edit mode, if poll doesn't exist, error is tracked and screen is closed`() = runTest {
|
fun `in edit mode, if poll doesn't exist, error is tracked and screen is closed`() = runTest {
|
||||||
val room = createFakeMatrixRoom(existingPoll = null)
|
val room = FakeMatrixRoom(
|
||||||
|
matrixTimeline = aPollTimeline()
|
||||||
|
)
|
||||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(AN_EVENT_ID), room = room)
|
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(AN_EVENT_ID), room = room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -475,7 +477,6 @@ class CreatePollPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private suspend fun TurbineTestContext<CreatePollState>.awaitDefaultItem() =
|
private suspend fun TurbineTestContext<CreatePollState>.awaitDefaultItem() =
|
||||||
awaitItem().apply {
|
awaitItem().apply {
|
||||||
assertThat(canSave).isFalse()
|
assertThat(canSave).isFalse()
|
||||||
|
|
@ -518,35 +519,8 @@ class CreatePollPresenterTest {
|
||||||
navigateUp = { navUpInvocationsCount++ },
|
navigateUp = { navUpInvocationsCount++ },
|
||||||
mode = mode,
|
mode = mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun createFakeMatrixRoom(
|
|
||||||
existingPoll: PollContent? = anExistingPoll(),
|
|
||||||
) = FakeMatrixRoom(
|
|
||||||
matrixTimeline = FakeMatrixTimeline(
|
|
||||||
initialTimelineItems = existingPoll?.let {
|
|
||||||
listOf(
|
|
||||||
MatrixTimelineItem.Event(
|
|
||||||
0,
|
|
||||||
anEventTimelineItem(
|
|
||||||
eventId = pollEventId,
|
|
||||||
content = it,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}.orEmpty()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun anExistingPoll() = aPollContent(
|
|
||||||
question = "Do you like polls?",
|
|
||||||
answers = persistentListOf(
|
|
||||||
PollAnswer("1", "Yes"),
|
|
||||||
PollAnswer("2", "No"),
|
|
||||||
PollAnswer("2", "Maybe"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun PollContent.expectedAnswersState() = answers.map { answer ->
|
private fun PollContent.expectedAnswersState() = answers.map { answer ->
|
||||||
Answer(
|
Answer(
|
||||||
text = answer.text,
|
text = answer.text,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
/*
|
||||||
|
* 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 app.cash.molecule.RecompositionMode
|
||||||
|
import app.cash.molecule.moleculeFlow
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.poll.api.actions.EndPollAction
|
||||||
|
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||||
|
import io.element.android.features.poll.impl.aPollTimeline
|
||||||
|
import io.element.android.features.poll.impl.anEndedPollContent
|
||||||
|
import io.element.android.features.poll.impl.anOngoingPollContent
|
||||||
|
import io.element.android.features.poll.impl.history.model.PollHistoryFilter
|
||||||
|
import io.element.android.features.poll.impl.history.model.PollHistoryItemsFactory
|
||||||
|
import io.element.android.features.poll.impl.model.DefaultPollContentStateFactory
|
||||||
|
import io.element.android.features.poll.test.actions.FakeEndPollAction
|
||||||
|
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
||||||
|
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runCurrent
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class PollHistoryPresenterTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
private val room = FakeMatrixRoom(
|
||||||
|
matrixTimeline = aPollTimeline(
|
||||||
|
polls = mapOf(
|
||||||
|
AN_EVENT_ID to anOngoingPollContent(),
|
||||||
|
AN_EVENT_ID_2 to anEndedPollContent()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - initial states`() = runTest {
|
||||||
|
val presenter = createPollHistoryPresenter(room = room)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.activeFilter).isEqualTo(PollHistoryFilter.ONGOING)
|
||||||
|
assertThat(state.pollHistoryItems.size).isEqualTo(0)
|
||||||
|
assertThat(state.isLoading).isTrue()
|
||||||
|
assertThat(state.hasMoreToLoad).isTrue()
|
||||||
|
}
|
||||||
|
consumeItemsUntilPredicate {
|
||||||
|
it.pollHistoryItems.size == 2
|
||||||
|
}.last().also { state ->
|
||||||
|
assertThat(state.pollHistoryItems.size).isEqualTo(2)
|
||||||
|
assertThat(state.pollHistoryItems.ongoing).hasSize(1)
|
||||||
|
assertThat(state.pollHistoryItems.past).hasSize(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - change filter scenario`() = runTest {
|
||||||
|
val presenter = createPollHistoryPresenter(room = room)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.activeFilter).isEqualTo(PollHistoryFilter.ONGOING)
|
||||||
|
state.eventSink(PollHistoryEvents.OnFilterSelected(PollHistoryFilter.PAST))
|
||||||
|
}
|
||||||
|
consumeItemsUntilPredicate {
|
||||||
|
it.activeFilter == PollHistoryFilter.PAST
|
||||||
|
}.last().also { state ->
|
||||||
|
state.eventSink(PollHistoryEvents.OnFilterSelected(PollHistoryFilter.ONGOING))
|
||||||
|
}
|
||||||
|
consumeItemsUntilPredicate {
|
||||||
|
it.activeFilter == PollHistoryFilter.ONGOING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
@Test
|
||||||
|
fun `present - poll actions scenario`() = runTest {
|
||||||
|
val sendPollResponseAction = FakeSendPollResponseAction()
|
||||||
|
val endPollAction = FakeEndPollAction()
|
||||||
|
val presenter = createPollHistoryPresenter(
|
||||||
|
sendPollResponseAction = sendPollResponseAction,
|
||||||
|
endPollAction = endPollAction
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val state = awaitItem()
|
||||||
|
state.eventSink(PollHistoryEvents.PollEndClicked(AN_EVENT_ID))
|
||||||
|
runCurrent()
|
||||||
|
endPollAction.verifyExecutionCount(1)
|
||||||
|
state.eventSink(PollHistoryEvents.PollAnswerSelected(AN_EVENT_ID, "answer"))
|
||||||
|
runCurrent()
|
||||||
|
sendPollResponseAction.verifyExecutionCount(1)
|
||||||
|
cancelAndConsumeRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - load more scenario`() = runTest {
|
||||||
|
val presenter = createPollHistoryPresenter(room = room)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
consumeItemsUntilPredicate {
|
||||||
|
it.pollHistoryItems.size == 2 && !it.isLoading
|
||||||
|
}.last().also { state ->
|
||||||
|
state.eventSink(PollHistoryEvents.LoadMore)
|
||||||
|
}
|
||||||
|
consumeItemsUntilPredicate {
|
||||||
|
it.isLoading
|
||||||
|
}
|
||||||
|
consumeItemsUntilPredicate {
|
||||||
|
!it.isLoading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createPollHistoryPresenter(
|
||||||
|
room: MatrixRoom = FakeMatrixRoom(),
|
||||||
|
appCoroutineScope: CoroutineScope = this,
|
||||||
|
endPollAction: EndPollAction = FakeEndPollAction(),
|
||||||
|
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
|
||||||
|
pollHistoryItemFactory: PollHistoryItemsFactory = PollHistoryItemsFactory(
|
||||||
|
pollContentStateFactory = DefaultPollContentStateFactory(FakeMatrixClient()),
|
||||||
|
daySeparatorFormatter = FakeDaySeparatorFormatter(),
|
||||||
|
dispatchers = testCoroutineDispatchers(),
|
||||||
|
),
|
||||||
|
): PollHistoryPresenter {
|
||||||
|
return PollHistoryPresenter(
|
||||||
|
room = room,
|
||||||
|
appCoroutineScope = appCoroutineScope,
|
||||||
|
sendPollResponseAction = sendPollResponseAction,
|
||||||
|
endPollAction = endPollAction,
|
||||||
|
pollHistoryItemFactory = pollHistoryItemFactory,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,289 @@
|
||||||
|
/*
|
||||||
|
* 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.pollcontent
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.poll.api.pollcontent.PollAnswerItem
|
||||||
|
import io.element.android.features.poll.api.pollcontent.PollContentState
|
||||||
|
import io.element.android.features.poll.impl.model.DefaultPollContentStateFactory
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||||
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_10
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_4
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_5
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_6
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_7
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_8
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_9
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.ImmutableMap
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class PollContentStateFactoryTest {
|
||||||
|
|
||||||
|
private val factory = DefaultPollContentStateFactory(FakeMatrixClient())
|
||||||
|
private val eventTimelineItem = anEventTimelineItem()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Disclosed poll - not ended, no votes`() = runTest {
|
||||||
|
val state = factory.create(eventTimelineItem, aPollContent())
|
||||||
|
val expectedState = aPollContentState()
|
||||||
|
assertThat(state).isEqualTo(expectedState)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Disclosed poll - not ended, some votes, including one from current user`() = runTest {
|
||||||
|
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||||
|
val state = factory.create(
|
||||||
|
eventTimelineItem, aPollContent(votes = votes)
|
||||||
|
)
|
||||||
|
val expectedState = aPollContentState(
|
||||||
|
answerItems = listOf(
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_1, votesCount = 3, percentage = 0.3f),
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, votesCount = 6, percentage = 0.6f),
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_3),
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_4, votesCount = 1, percentage = 0.1f),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertThat(state).isEqualTo(expectedState)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Disclosed poll - ended, no votes, no winner`() = runTest {
|
||||||
|
val state = factory.create(eventTimelineItem, aPollContent(endTime = 1UL))
|
||||||
|
val expectedState = aPollContentState().let {
|
||||||
|
it.copy(
|
||||||
|
answerItems = it.answerItems.map { answerItem -> answerItem.copy(isEnabled = false) }.toImmutableList(),
|
||||||
|
isPollEnded = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertThat(state).isEqualTo(expectedState)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Disclosed poll - ended, some votes, including one from current user (winner)`() = runTest {
|
||||||
|
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||||
|
val state = factory.create(
|
||||||
|
eventTimelineItem, aPollContent(votes = votes, endTime = 1UL)
|
||||||
|
)
|
||||||
|
val expectedState = aPollContentState(
|
||||||
|
answerItems = listOf(
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, votesCount = 3, percentage = 0.3f),
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, isWinner = true, votesCount = 6, percentage = 0.6f),
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false),
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, votesCount = 1, percentage = 0.1f),
|
||||||
|
),
|
||||||
|
isEnded = true,
|
||||||
|
)
|
||||||
|
assertThat(state).isEqualTo(expectedState)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Disclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest {
|
||||||
|
val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||||
|
val state = factory.create(
|
||||||
|
eventTimelineItem, aPollContent(votes = votes, endTime = 1UL)
|
||||||
|
)
|
||||||
|
val expectedState = aPollContentState(
|
||||||
|
answerItems = listOf(
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f),
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, votesCount = 2, percentage = 0.2f),
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false),
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f),
|
||||||
|
),
|
||||||
|
isEnded = true,
|
||||||
|
)
|
||||||
|
assertThat(state).isEqualTo(expectedState)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Undisclosed poll - not ended, no votes`() = runTest {
|
||||||
|
val state = factory.create(eventTimelineItem, aPollContent(PollKind.Undisclosed))
|
||||||
|
val expectedState = aPollContentState(pollKind = PollKind.Undisclosed).let {
|
||||||
|
it.copy(
|
||||||
|
answerItems = it.answerItems.map { answerItem -> answerItem.copy(isDisclosed = false) }.toImmutableList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertThat(state).isEqualTo(expectedState)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Undisclosed poll - not ended, some votes, including one from current user`() = runTest {
|
||||||
|
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||||
|
val state = factory.create(
|
||||||
|
eventTimelineItem, aPollContent(PollKind.Undisclosed, votes = votes)
|
||||||
|
)
|
||||||
|
val expectedState = aPollContentState(
|
||||||
|
pollKind = PollKind.Undisclosed,
|
||||||
|
answerItems = listOf(
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_1, isDisclosed = false, votesCount = 3, percentage = 0.3f),
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_2, isDisclosed = false, isSelected = true, votesCount = 6, percentage = 0.6f),
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_3, isDisclosed = false),
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_4, isDisclosed = false, votesCount = 1, percentage = 0.1f),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertThat(state).isEqualTo(expectedState)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Undisclosed poll - ended, no votes, no winner`() = runTest {
|
||||||
|
val state = factory.create(eventTimelineItem, aPollContent(PollKind.Undisclosed, endTime = 1UL))
|
||||||
|
val expectedState = aPollContentState(
|
||||||
|
isEnded = true,
|
||||||
|
pollKind = PollKind.Undisclosed
|
||||||
|
).let {
|
||||||
|
it.copy(
|
||||||
|
answerItems = it.answerItems.map { answerItem -> answerItem.copy(isDisclosed = true, isEnabled = false) }.toImmutableList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertThat(state).isEqualTo(expectedState)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Undisclosed poll - ended, some votes, including one from current user (winner)`() = runTest {
|
||||||
|
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||||
|
val state = factory.create(
|
||||||
|
eventTimelineItem, aPollContent(PollKind.Undisclosed, votes = votes, endTime = 1UL)
|
||||||
|
)
|
||||||
|
val expectedState = aPollContentState(
|
||||||
|
pollKind = PollKind.Undisclosed,
|
||||||
|
answerItems = listOf(
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, votesCount = 3, percentage = 0.3f),
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, isWinner = true, votesCount = 6, percentage = 0.6f),
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false),
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, votesCount = 1, percentage = 0.1f),
|
||||||
|
),
|
||||||
|
isEnded = true,
|
||||||
|
)
|
||||||
|
assertThat(state).isEqualTo(expectedState)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Undisclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest {
|
||||||
|
val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||||
|
val state = factory.create(
|
||||||
|
eventTimelineItem, aPollContent(PollKind.Undisclosed, votes = votes, endTime = 1UL)
|
||||||
|
)
|
||||||
|
val expectedState = aPollContentState(
|
||||||
|
pollKind = PollKind.Undisclosed,
|
||||||
|
answerItems = listOf(
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f),
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, votesCount = 2, percentage = 0.2f),
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false),
|
||||||
|
aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f),
|
||||||
|
),
|
||||||
|
isEnded = true,
|
||||||
|
)
|
||||||
|
assertThat(state).isEqualTo(expectedState)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `eventId is populated`() = runTest {
|
||||||
|
val state = factory.create(eventTimelineItem, aPollContent())
|
||||||
|
assertThat(state.eventId).isEqualTo(eventTimelineItem.eventId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aPollContent(
|
||||||
|
pollKind: PollKind = PollKind.Disclosed,
|
||||||
|
votes: ImmutableMap<String, ImmutableList<UserId>> = persistentMapOf(),
|
||||||
|
endTime: ULong? = null,
|
||||||
|
): PollContent = PollContent(
|
||||||
|
question = A_POLL_QUESTION,
|
||||||
|
kind = pollKind,
|
||||||
|
maxSelections = 1UL,
|
||||||
|
answers = persistentListOf(A_POLL_ANSWER_1, A_POLL_ANSWER_2, A_POLL_ANSWER_3, A_POLL_ANSWER_4),
|
||||||
|
votes = votes,
|
||||||
|
endTime = endTime,
|
||||||
|
isEdited = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun aPollContentState(
|
||||||
|
eventId: EventId? = AN_EVENT_ID,
|
||||||
|
pollKind: PollKind = PollKind.Disclosed,
|
||||||
|
answerItems: List<PollAnswerItem> = listOf(
|
||||||
|
aPollAnswerItem(A_POLL_ANSWER_1),
|
||||||
|
aPollAnswerItem(A_POLL_ANSWER_2),
|
||||||
|
aPollAnswerItem(A_POLL_ANSWER_3),
|
||||||
|
aPollAnswerItem(A_POLL_ANSWER_4),
|
||||||
|
),
|
||||||
|
isEnded: Boolean = false,
|
||||||
|
isMine: Boolean = false,
|
||||||
|
isEditable: Boolean = false,
|
||||||
|
question: String = A_POLL_QUESTION,
|
||||||
|
) = PollContentState(
|
||||||
|
eventId = eventId,
|
||||||
|
question = question,
|
||||||
|
answerItems = answerItems.toImmutableList(),
|
||||||
|
pollKind = pollKind,
|
||||||
|
isPollEditable = isEditable,
|
||||||
|
isPollEnded = isEnded,
|
||||||
|
isMine = isMine,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun aPollAnswerItem(
|
||||||
|
answer: PollAnswer,
|
||||||
|
isSelected: Boolean = false,
|
||||||
|
isEnabled: Boolean = true,
|
||||||
|
isWinner: Boolean = false,
|
||||||
|
isDisclosed: Boolean = true,
|
||||||
|
votesCount: Int = 0,
|
||||||
|
percentage: Float = 0f,
|
||||||
|
) = PollAnswerItem(
|
||||||
|
answer = answer,
|
||||||
|
isSelected = isSelected,
|
||||||
|
isEnabled = isEnabled,
|
||||||
|
isWinner = isWinner,
|
||||||
|
isDisclosed = isDisclosed,
|
||||||
|
votesCount = votesCount,
|
||||||
|
percentage = percentage,
|
||||||
|
)
|
||||||
|
|
||||||
|
private companion object TestData {
|
||||||
|
private const val A_POLL_QUESTION = "What is your favorite food?"
|
||||||
|
private val A_POLL_ANSWER_1 = PollAnswer("id_1", "Pizza")
|
||||||
|
private val A_POLL_ANSWER_2 = PollAnswer("id_2", "Pasta")
|
||||||
|
private val A_POLL_ANSWER_3 = PollAnswer("id_3", "French Fries")
|
||||||
|
private val A_POLL_ANSWER_4 = PollAnswer("id_4", "Hamburger")
|
||||||
|
|
||||||
|
private val MY_USER_WINNING_VOTES = persistentMapOf(
|
||||||
|
A_POLL_ANSWER_1 to persistentListOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4),
|
||||||
|
A_POLL_ANSWER_2 to persistentListOf(A_USER_ID /* my vote */, A_USER_ID_5, A_USER_ID_6, A_USER_ID_7, A_USER_ID_8, A_USER_ID_9), // winner
|
||||||
|
A_POLL_ANSWER_3 to persistentListOf(),
|
||||||
|
A_POLL_ANSWER_4 to persistentListOf(A_USER_ID_10),
|
||||||
|
)
|
||||||
|
private val OTHER_WINNING_VOTES = persistentMapOf(
|
||||||
|
A_POLL_ANSWER_1 to persistentListOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4, A_USER_ID_5), // winner
|
||||||
|
A_POLL_ANSWER_2 to persistentListOf(A_USER_ID /* my vote */, A_USER_ID_6),
|
||||||
|
A_POLL_ANSWER_3 to persistentListOf(),
|
||||||
|
A_POLL_ANSWER_4 to persistentListOf(A_USER_ID_7, A_USER_ID_8, A_USER_ID_9, A_USER_ID_10), // winner
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
29
features/poll/test/build.gradle.kts
Normal file
29
features/poll/test/build.gradle.kts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.features.poll.test"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
api(projects.features.poll.api)
|
||||||
|
implementation(libs.kotlinx.collections.immutable)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* 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.test.actions
|
||||||
|
|
||||||
|
import io.element.android.features.poll.api.actions.EndPollAction
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
||||||
|
class FakeEndPollAction : EndPollAction {
|
||||||
|
|
||||||
|
private var executionCount = 0
|
||||||
|
|
||||||
|
fun verifyExecutionCount(count: Int) {
|
||||||
|
assert(executionCount == count)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun execute(pollStartId: EventId): Result<Unit> {
|
||||||
|
executionCount++
|
||||||
|
return Result.success(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* 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.test.actions
|
||||||
|
|
||||||
|
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
||||||
|
class FakeSendPollResponseAction : SendPollResponseAction {
|
||||||
|
|
||||||
|
private var executionCount = 0
|
||||||
|
|
||||||
|
fun verifyExecutionCount(count: Int) {
|
||||||
|
assert(executionCount == count)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun execute(pollStartId: EventId, answerId: String): Result<Unit> {
|
||||||
|
executionCount++
|
||||||
|
return Result.success(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.poll.test.pollcontent
|
||||||
|
|
||||||
|
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.matrix.api.timeline.item.event.EventTimelineItem
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
class FakePollContentStateFactory : PollContentStateFactory {
|
||||||
|
|
||||||
|
override suspend fun create(event: EventTimelineItem, content: PollContent): PollContentState {
|
||||||
|
return PollContentState(
|
||||||
|
eventId = event.eventId,
|
||||||
|
question = content.question,
|
||||||
|
answerItems = emptyList<PollAnswerItem>().toImmutableList(),
|
||||||
|
pollKind = content.kind,
|
||||||
|
isPollEditable = event.isEditable,
|
||||||
|
isPollEnded = content.endTime != null,
|
||||||
|
isMine = event.isOwn
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -53,6 +53,7 @@ dependencies {
|
||||||
implementation(projects.features.leaveroom.api)
|
implementation(projects.features.leaveroom.api)
|
||||||
implementation(projects.features.createroom.api)
|
implementation(projects.features.createroom.api)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
|
implementation(projects.features.poll.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.poll.api.history.PollHistoryEntryPoint
|
||||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||||
import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode
|
import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode
|
||||||
import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode
|
import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode
|
||||||
|
|
@ -52,6 +53,7 @@ import kotlinx.parcelize.Parcelize
|
||||||
class RoomDetailsFlowNode @AssistedInject constructor(
|
class RoomDetailsFlowNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val pollHistoryEntryPoint: PollHistoryEntryPoint,
|
||||||
) : BackstackNode<RoomDetailsFlowNode.NavTarget>(
|
) : BackstackNode<RoomDetailsFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = plugins.filterIsInstance<RoomDetailsEntryPoint.Params>().first().initialElement.toNavTarget(),
|
initialElement = plugins.filterIsInstance<RoomDetailsEntryPoint.Params>().first().initialElement.toNavTarget(),
|
||||||
|
|
@ -88,6 +90,9 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class AvatarPreview(val name: String, val avatarUrl: String) : NavTarget
|
data class AvatarPreview(val name: String, val avatarUrl: String) : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object PollHistory : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
|
@ -113,6 +118,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
||||||
override fun openAvatarPreview(name: String, url: String) {
|
override fun openAvatarPreview(name: String, url: String) {
|
||||||
backstack.push(NavTarget.AvatarPreview(name, url))
|
backstack.push(NavTarget.AvatarPreview(name, url))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun openPollHistory() {
|
||||||
|
backstack.push(NavTarget.PollHistory)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
createNode<RoomDetailsNode>(buildContext, listOf(roomDetailsCallback))
|
createNode<RoomDetailsNode>(buildContext, listOf(roomDetailsCallback))
|
||||||
}
|
}
|
||||||
|
|
@ -178,6 +187,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
createNode<AvatarPreviewNode>(buildContext, listOf(input))
|
createNode<AvatarPreviewNode>(buildContext, listOf(input))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is NavTarget.PollHistory -> {
|
||||||
|
pollHistoryEntryPoint.createNode(this, buildContext)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ class RoomDetailsNode @AssistedInject constructor(
|
||||||
fun editRoomDetails()
|
fun editRoomDetails()
|
||||||
fun openRoomNotificationSettings()
|
fun openRoomNotificationSettings()
|
||||||
fun openAvatarPreview(name: String, url: String)
|
fun openAvatarPreview(name: String, url: String)
|
||||||
|
fun openPollHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callbacks = plugins<Callback>()
|
private val callbacks = plugins<Callback>()
|
||||||
|
|
@ -77,6 +78,10 @@ class RoomDetailsNode @AssistedInject constructor(
|
||||||
callbacks.forEach { it.openInviteMembers() }
|
callbacks.forEach { it.openInviteMembers() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openPollHistory() {
|
||||||
|
callbacks.forEach { it.openPollHistory() }
|
||||||
|
}
|
||||||
|
|
||||||
private fun onShareRoom(context: Context) {
|
private fun onShareRoom(context: Context) {
|
||||||
val alias = room.alias ?: room.alternativeAliases.firstOrNull()
|
val alias = room.alias ?: room.alternativeAliases.firstOrNull()
|
||||||
val permalinkResult = alias?.let { PermalinkBuilder.permalinkForRoomAlias(it) }
|
val permalinkResult = alias?.let { PermalinkBuilder.permalinkForRoomAlias(it) }
|
||||||
|
|
@ -146,6 +151,7 @@ class RoomDetailsNode @AssistedInject constructor(
|
||||||
openRoomNotificationSettings = ::openRoomNotificationSettings,
|
openRoomNotificationSettings = ::openRoomNotificationSettings,
|
||||||
invitePeople = ::invitePeople,
|
invitePeople = ::invitePeople,
|
||||||
openAvatarPreview = ::openAvatarPreview,
|
openAvatarPreview = ::openAvatarPreview,
|
||||||
|
openPollHistory = ::openPollHistory,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ fun RoomDetailsView(
|
||||||
openRoomNotificationSettings: () -> Unit,
|
openRoomNotificationSettings: () -> Unit,
|
||||||
invitePeople: () -> Unit,
|
invitePeople: () -> Unit,
|
||||||
openAvatarPreview: (name: String, url: String) -> Unit,
|
openAvatarPreview: (name: String, url: String) -> Unit,
|
||||||
|
openPollHistory: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
fun onShareMember() {
|
fun onShareMember() {
|
||||||
|
|
@ -175,6 +176,10 @@ fun RoomDetailsView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PollsSection(
|
||||||
|
openPollHistory = openPollHistory
|
||||||
|
)
|
||||||
|
|
||||||
if (state.isEncrypted) {
|
if (state.isEncrypted) {
|
||||||
SecuritySection()
|
SecuritySection()
|
||||||
}
|
}
|
||||||
|
|
@ -379,6 +384,20 @@ private fun InviteSection(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PollsSection(
|
||||||
|
openPollHistory: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
PreferenceCategory(modifier = modifier) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(stringResource(R.string.screen_polls_history_title)) },
|
||||||
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Polls)),
|
||||||
|
onClick = openPollHistory,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SecuritySection(modifier: Modifier = Modifier) {
|
private fun SecuritySection(modifier: Modifier = Modifier) {
|
||||||
PreferenceCategory(title = stringResource(R.string.screen_room_details_security_title), modifier = modifier) {
|
PreferenceCategory(title = stringResource(R.string.screen_room_details_security_title), modifier = modifier) {
|
||||||
|
|
@ -424,5 +443,6 @@ private fun ContentToPreview(state: RoomDetailsState) {
|
||||||
openRoomNotificationSettings = {},
|
openRoomNotificationSettings = {},
|
||||||
invitePeople = {},
|
invitePeople = {},
|
||||||
openAvatarPreview = { _, _ -> },
|
openAvatarPreview = { _, _ -> },
|
||||||
|
openPollHistory = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
<item quantity="other">"%1$d people"</item>
|
<item quantity="other">"%1$d people"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"An error occurred while updating the notification setting."</string>
|
<string name="screen_notification_settings_edit_failed_updating_default_mode">"An error occurred while updating the notification setting."</string>
|
||||||
|
<string name="screen_polls_history_title">"Polls"</string>
|
||||||
<string name="screen_notification_settings_mentions_only_disclaimer">"Your homeserver does not support this option in encrypted rooms, you may not get notified in some rooms."</string>
|
<string name="screen_notification_settings_mentions_only_disclaimer">"Your homeserver does not support this option in encrypted rooms, you may not get notified in some rooms."</string>
|
||||||
<string name="screen_room_details_add_topic_title">"Add topic"</string>
|
<string name="screen_room_details_add_topic_title">"Add topic"</string>
|
||||||
<string name="screen_room_details_already_a_member">"Already a member"</string>
|
<string name="screen_room_details_already_a_member">"Already a member"</string>
|
||||||
|
|
|
||||||
|
|
@ -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.libraries.designsystem.theme.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.SegmentedButton
|
||||||
|
import androidx.compose.material3.SegmentedButtonDefaults
|
||||||
|
import androidx.compose.material3.SingleChoiceSegmentedButtonRowScope
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SingleChoiceSegmentedButtonRowScope.SegmentedButton(
|
||||||
|
index: Int,
|
||||||
|
count: Int,
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
enabled: Boolean = true,
|
||||||
|
) {
|
||||||
|
SegmentedButton(
|
||||||
|
selected = selected,
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
enabled = enabled,
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(index = index, count = count),
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = ElementTheme.typography.fontBodyMdMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = SegmentedButtonDefaults.colors(
|
||||||
|
activeContainerColor = ElementTheme.materialColors.primary,
|
||||||
|
activeContentColor = ElementTheme.materialColors.onPrimary,
|
||||||
|
activeBorderColor = ElementTheme.materialColors.primary,
|
||||||
|
inactiveContainerColor = ElementTheme.materialColors.surface,
|
||||||
|
inactiveContentColor = ElementTheme.materialColors.onSurface,
|
||||||
|
inactiveBorderColor = ElementTheme.materialColors.primary,
|
||||||
|
disabledActiveContainerColor = ElementTheme.colors.bgActionPrimaryDisabled,
|
||||||
|
disabledActiveContentColor = ElementTheme.colors.textOnSolidPrimary,
|
||||||
|
disabledActiveBorderColor = ElementTheme.colors.bgActionPrimaryDisabled,
|
||||||
|
disabledInactiveContainerColor = ElementTheme.materialColors.surface,
|
||||||
|
disabledInactiveContentColor = ElementTheme.colors.textDisabled,
|
||||||
|
disabledInactiveBorderColor = Color.Transparent,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -50,9 +50,9 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageT
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.aPollContent
|
import io.element.android.libraries.matrix.test.timeline.aPollContent
|
||||||
import io.element.android.libraries.matrix.test.room.aProfileChangeMessageContent
|
import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageContent
|
||||||
import io.element.android.libraries.matrix.test.room.anEventTimelineItem
|
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||||
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
|
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
|
||||||
|
|
@ -241,7 +241,7 @@ interface MatrixRoom : Closeable {
|
||||||
*/
|
*/
|
||||||
fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver>
|
fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver>
|
||||||
|
|
||||||
suspend fun pollHistory(): MatrixTimeline
|
fun pollHistory(): MatrixTimeline
|
||||||
|
|
||||||
override fun close() = destroy()
|
override fun close() = destroy()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,21 @@ interface MatrixTimeline : AutoCloseable {
|
||||||
val beginningOfRoomReached: Boolean,
|
val beginningOfRoomReached: Boolean,
|
||||||
) {
|
) {
|
||||||
val canBackPaginate = !isBackPaginating && hasMoreToLoadBackwards
|
val canBackPaginate = !isBackPaginating && hasMoreToLoadBackwards
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val Initial = PaginationState(
|
||||||
|
isBackPaginating = false,
|
||||||
|
hasMoreToLoadBackwards = true,
|
||||||
|
beginningOfRoomReached = false
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val paginationState: StateFlow<PaginationState>
|
val paginationState: StateFlow<PaginationState>
|
||||||
val timelineItems: Flow<List<MatrixTimelineItem>>
|
val timelineItems: Flow<List<MatrixTimelineItem>>
|
||||||
|
|
||||||
|
suspend fun paginateBackwards(requestSize: Int): Result<Unit>
|
||||||
suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit>
|
suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit>
|
||||||
suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit>
|
suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit>
|
||||||
|
|
||||||
suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result<Unit>
|
suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result<Unit>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import io.element.android.libraries.matrix.api.room.StateEventType
|
||||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||||
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
|
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
||||||
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
|
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
|
||||||
|
|
@ -50,6 +51,7 @@ import io.element.android.libraries.matrix.impl.media.toMSC3246range
|
||||||
import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService
|
import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService
|
||||||
import io.element.android.libraries.matrix.impl.poll.toInner
|
import io.element.android.libraries.matrix.impl.poll.toInner
|
||||||
import io.element.android.libraries.matrix.impl.room.location.toInner
|
import io.element.android.libraries.matrix.impl.room.location.toInner
|
||||||
|
import io.element.android.libraries.matrix.impl.timeline.AsyncMatrixTimeline
|
||||||
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
|
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
|
||||||
import io.element.android.libraries.matrix.impl.util.destroyAll
|
import io.element.android.libraries.matrix.impl.util.destroyAll
|
||||||
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
||||||
|
|
@ -70,14 +72,12 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.rustcomponents.sdk.EventTimelineItem
|
import org.matrix.rustcomponents.sdk.EventTimelineItem
|
||||||
import org.matrix.rustcomponents.sdk.Room
|
|
||||||
import org.matrix.rustcomponents.sdk.RoomInfo
|
import org.matrix.rustcomponents.sdk.RoomInfo
|
||||||
import org.matrix.rustcomponents.sdk.RoomInfoListener
|
import org.matrix.rustcomponents.sdk.RoomInfoListener
|
||||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||||
import org.matrix.rustcomponents.sdk.RoomMember
|
import org.matrix.rustcomponents.sdk.RoomMember
|
||||||
import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation
|
import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation
|
||||||
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
|
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
|
||||||
import org.matrix.rustcomponents.sdk.Timeline
|
|
||||||
import org.matrix.rustcomponents.sdk.WidgetCapabilities
|
import org.matrix.rustcomponents.sdk.WidgetCapabilities
|
||||||
import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider
|
import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider
|
||||||
import org.matrix.rustcomponents.sdk.messageEventContentFromHtml
|
import org.matrix.rustcomponents.sdk.messageEventContentFromHtml
|
||||||
|
|
@ -85,14 +85,16 @@ import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
|
||||||
import org.matrix.rustcomponents.sdk.use
|
import org.matrix.rustcomponents.sdk.use
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import org.matrix.rustcomponents.sdk.Room as InnerRoom
|
||||||
|
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class RustMatrixRoom(
|
class RustMatrixRoom(
|
||||||
override val sessionId: SessionId,
|
override val sessionId: SessionId,
|
||||||
private val isKeyBackupEnabled: Boolean,
|
private val isKeyBackupEnabled: Boolean,
|
||||||
private val roomListItem: RoomListItem,
|
private val roomListItem: RoomListItem,
|
||||||
private val innerRoom: Room,
|
private val innerRoom: InnerRoom,
|
||||||
private val innerTimeline: Timeline,
|
private val innerTimeline: InnerTimeline,
|
||||||
private val roomNotificationSettingsService: RustNotificationSettingsService,
|
private val roomNotificationSettingsService: RustNotificationSettingsService,
|
||||||
sessionCoroutineScope: CoroutineScope,
|
sessionCoroutineScope: CoroutineScope,
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
|
@ -130,15 +132,9 @@ class RustMatrixRoom(
|
||||||
private val _roomNotificationSettingsStateFlow = MutableStateFlow<MatrixRoomNotificationSettingsState>(MatrixRoomNotificationSettingsState.Unknown)
|
private val _roomNotificationSettingsStateFlow = MutableStateFlow<MatrixRoomNotificationSettingsState>(MatrixRoomNotificationSettingsState.Unknown)
|
||||||
override val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState> = _roomNotificationSettingsStateFlow
|
override val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState> = _roomNotificationSettingsStateFlow
|
||||||
|
|
||||||
override val timeline = RustMatrixTimeline(
|
override val timeline = createMatrixTimeline(innerTimeline) {
|
||||||
isKeyBackupEnabled = isKeyBackupEnabled,
|
_syncUpdateFlow.value = systemClock.epochMillis()
|
||||||
matrixRoom = this,
|
}
|
||||||
innerTimeline = innerTimeline,
|
|
||||||
roomCoroutineScope = roomCoroutineScope,
|
|
||||||
dispatcher = roomDispatcher,
|
|
||||||
lastLoginTimestamp = sessionData.loginTimestamp,
|
|
||||||
onNewSyncedEvent = { _syncUpdateFlow.value = systemClock.epochMillis() }
|
|
||||||
)
|
|
||||||
|
|
||||||
override val membersStateFlow: StateFlow<MatrixRoomMembersState> = _membersStateFlow.asStateFlow()
|
override val membersStateFlow: StateFlow<MatrixRoomMembersState> = _membersStateFlow.asStateFlow()
|
||||||
|
|
||||||
|
|
@ -150,7 +146,7 @@ class RustMatrixRoom(
|
||||||
|
|
||||||
override fun destroy() {
|
override fun destroy() {
|
||||||
roomCoroutineScope.cancel()
|
roomCoroutineScope.cancel()
|
||||||
innerTimeline.destroy()
|
timeline.close()
|
||||||
innerRoom.destroy()
|
innerRoom.destroy()
|
||||||
roomListItem.destroy()
|
roomListItem.destroy()
|
||||||
specialModeEventTimelineItem?.destroy()
|
specialModeEventTimelineItem?.destroy()
|
||||||
|
|
@ -564,15 +560,13 @@ class RustMatrixRoom(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun pollHistory() = RustMatrixTimeline(
|
override fun pollHistory() = AsyncMatrixTimeline(
|
||||||
isKeyBackupEnabled = isKeyBackupEnabled,
|
coroutineScope = roomCoroutineScope,
|
||||||
matrixRoom = this,
|
dispatcher = roomDispatcher
|
||||||
innerTimeline = innerRoom.pollHistory(),
|
) {
|
||||||
roomCoroutineScope = roomCoroutineScope,
|
val innerTimeline = innerRoom.pollHistory()
|
||||||
dispatcher = roomDispatcher,
|
createMatrixTimeline(innerTimeline)
|
||||||
lastLoginTimestamp = sessionData.loginTimestamp,
|
}
|
||||||
onNewSyncedEvent = { _syncUpdateFlow.value = systemClock.epochMillis() }
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun sendAttachment(files: List<File>, handle: () -> SendAttachmentJoinHandle): Result<MediaUploadHandler> {
|
private fun sendAttachment(files: List<File>, handle: () -> SendAttachmentJoinHandle): Result<MediaUploadHandler> {
|
||||||
return runCatching {
|
return runCatching {
|
||||||
|
|
@ -580,6 +574,21 @@ class RustMatrixRoom(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createMatrixTimeline(
|
||||||
|
timeline: InnerTimeline,
|
||||||
|
onNewSyncedEvent: () -> Unit = {},
|
||||||
|
): MatrixTimeline {
|
||||||
|
return RustMatrixTimeline(
|
||||||
|
isKeyBackupEnabled = isKeyBackupEnabled,
|
||||||
|
matrixRoom = this,
|
||||||
|
roomCoroutineScope = roomCoroutineScope,
|
||||||
|
dispatcher = roomDispatcher,
|
||||||
|
lastLoginTimestamp = sessionData.loginTimestamp,
|
||||||
|
onNewSyncedEvent = onNewSyncedEvent,
|
||||||
|
innerTimeline = timeline,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun messageEventContentFromParts(body: String, htmlBody: String?): RoomMessageEventContentWithoutRelation =
|
private fun messageEventContentFromParts(body: String, htmlBody: String?): RoomMessageEventContentWithoutRelation =
|
||||||
if (htmlBody != null) {
|
if (htmlBody != null) {
|
||||||
messageEventContentFromHtml(body, htmlBody)
|
messageEventContentFromHtml(body, htmlBody)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* 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.timeline
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
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.ReceiptType
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is a wrapper around a [MatrixTimeline] that will be created asynchronously.
|
||||||
|
*/
|
||||||
|
class AsyncMatrixTimeline(
|
||||||
|
coroutineScope: CoroutineScope,
|
||||||
|
dispatcher: CoroutineDispatcher,
|
||||||
|
private val timelineProvider: suspend () -> MatrixTimeline
|
||||||
|
) : MatrixTimeline {
|
||||||
|
|
||||||
|
private val _timelineItems: MutableStateFlow<List<MatrixTimelineItem>> =
|
||||||
|
MutableStateFlow(emptyList())
|
||||||
|
|
||||||
|
private val _paginationState = MutableStateFlow(
|
||||||
|
MatrixTimeline.PaginationState.Initial
|
||||||
|
)
|
||||||
|
private val timeline = coroutineScope.async(context = dispatcher, start = CoroutineStart.LAZY) {
|
||||||
|
timelineProvider()
|
||||||
|
}
|
||||||
|
private val closeSignal = CompletableDeferred<Unit>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
coroutineScope.launch {
|
||||||
|
val delegateTimeline = timeline.await()
|
||||||
|
delegateTimeline.timelineItems
|
||||||
|
.onEach { _timelineItems.value = it }
|
||||||
|
.launchIn(this)
|
||||||
|
delegateTimeline.paginationState
|
||||||
|
.onEach { _paginationState.value = it }
|
||||||
|
.launchIn(this)
|
||||||
|
|
||||||
|
launch {
|
||||||
|
withContext(NonCancellable) {
|
||||||
|
closeSignal.await()
|
||||||
|
Timber.d("Close delegate")
|
||||||
|
delegateTimeline.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val paginationState: StateFlow<MatrixTimeline.PaginationState> = _paginationState
|
||||||
|
override val timelineItems: Flow<List<MatrixTimelineItem>> = _timelineItems
|
||||||
|
|
||||||
|
override suspend fun paginateBackwards(requestSize: Int): Result<Unit> {
|
||||||
|
return timeline.await().paginateBackwards(requestSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> {
|
||||||
|
return timeline.await().paginateBackwards(requestSize, untilNumberOfItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit> {
|
||||||
|
return timeline.await().fetchDetailsForEvent(eventId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result<Unit> {
|
||||||
|
return timeline.await().sendReadReceipt(eventId, receiptType)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
closeSignal.complete(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -73,11 +73,7 @@ class RustMatrixTimeline(
|
||||||
MutableStateFlow(emptyList())
|
MutableStateFlow(emptyList())
|
||||||
|
|
||||||
private val _paginationState = MutableStateFlow(
|
private val _paginationState = MutableStateFlow(
|
||||||
MatrixTimeline.PaginationState(
|
MatrixTimeline.PaginationState.Initial
|
||||||
hasMoreToLoadBackwards = true,
|
|
||||||
isBackPaginating = false,
|
|
||||||
beginningOfRoomReached = false,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private val encryptedHistoryPostProcessor = TimelineEncryptedHistoryPostProcessor(
|
private val encryptedHistoryPostProcessor = TimelineEncryptedHistoryPostProcessor(
|
||||||
|
|
@ -131,7 +127,11 @@ class RustMatrixTimeline(
|
||||||
|
|
||||||
private suspend fun fetchMembers() = withContext(dispatcher) {
|
private suspend fun fetchMembers() = withContext(dispatcher) {
|
||||||
initLatch.await()
|
initLatch.await()
|
||||||
innerTimeline.fetchMembers()
|
try {
|
||||||
|
innerTimeline.fetchMembers()
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Timber.e(exception, "Error fetching members for room ${matrixRoom.roomId}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
@ -193,15 +193,28 @@ class RustMatrixTimeline(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> = withContext(dispatcher) {
|
override suspend fun paginateBackwards(requestSize: Int): Result<Unit> {
|
||||||
|
val paginationOptions = PaginationOptions.SimpleRequest(
|
||||||
|
eventLimit = requestSize.toUShort(),
|
||||||
|
waitForToken = true,
|
||||||
|
)
|
||||||
|
return paginateBackwards(paginationOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> {
|
||||||
|
val paginationOptions = PaginationOptions.UntilNumItems(
|
||||||
|
eventLimit = requestSize.toUShort(),
|
||||||
|
items = untilNumberOfItems.toUShort(),
|
||||||
|
waitForToken = true,
|
||||||
|
)
|
||||||
|
return paginateBackwards(paginationOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun paginateBackwards(paginationOptions: PaginationOptions): Result<Unit> = withContext(dispatcher) {
|
||||||
|
initLatch.await()
|
||||||
runCatching {
|
runCatching {
|
||||||
if (!canBackPaginate()) throw TimelineException.CannotPaginate
|
if (!canBackPaginate()) throw TimelineException.CannotPaginate
|
||||||
Timber.v("Start back paginating for room ${matrixRoom.roomId} ")
|
Timber.v("Start back paginating for room ${matrixRoom.roomId} ")
|
||||||
val paginationOptions = PaginationOptions.UntilNumItems(
|
|
||||||
eventLimit = requestSize.toUShort(),
|
|
||||||
items = untilNumberOfItems.toUShort(),
|
|
||||||
waitForToken = true,
|
|
||||||
)
|
|
||||||
innerTimeline.paginateBackwards(paginationOptions)
|
innerTimeline.paginateBackwards(paginationOptions)
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
if (error is TimelineException.CannotPaginate) {
|
if (error is TimelineException.CannotPaginate) {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
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.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||||
import io.element.android.libraries.matrix.test.room.anEventTimelineItem
|
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
|
||||||
|
|
@ -431,7 +431,8 @@ class FakeMatrixRoom(
|
||||||
): Result<String> = generateWidgetWebViewUrlResult
|
): Result<String> = generateWidgetWebViewUrlResult
|
||||||
|
|
||||||
override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver> = getWidgetDriverResult
|
override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver> = getWidgetDriverResult
|
||||||
override suspend fun pollHistory(): MatrixTimeline {
|
|
||||||
|
override fun pollHistory(): MatrixTimeline {
|
||||||
return FakeMatrixTimeline()
|
return FakeMatrixTimeline()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,35 +18,17 @@ package io.element.android.libraries.matrix.test.room
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
|
||||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
|
||||||
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.PollContent
|
|
||||||
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.Receipt
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.collections.immutable.persistentMapOf
|
|
||||||
|
|
||||||
fun aRoomSummaryFilled(
|
fun aRoomSummaryFilled(
|
||||||
roomId: RoomId = A_ROOM_ID,
|
roomId: RoomId = A_ROOM_ID,
|
||||||
|
|
@ -101,96 +83,3 @@ fun aRoomMessage(
|
||||||
sender = userId,
|
sender = userId,
|
||||||
originServerTs = timestamp,
|
originServerTs = timestamp,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun anEventTimelineItem(
|
|
||||||
eventId: EventId = AN_EVENT_ID,
|
|
||||||
transactionId: TransactionId? = null,
|
|
||||||
isEditable: Boolean = false,
|
|
||||||
isLocal: Boolean = false,
|
|
||||||
isOwn: Boolean = false,
|
|
||||||
isRemote: Boolean = false,
|
|
||||||
localSendState: LocalEventSendState? = null,
|
|
||||||
reactions: ImmutableList<EventReaction> = persistentListOf(),
|
|
||||||
receipts: ImmutableList<Receipt> = persistentListOf(),
|
|
||||||
sender: UserId = A_USER_ID,
|
|
||||||
senderProfile: ProfileTimelineDetails = aProfileTimelineDetails(),
|
|
||||||
timestamp: Long = 0L,
|
|
||||||
content: EventContent = aProfileChangeMessageContent(),
|
|
||||||
debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(),
|
|
||||||
) = EventTimelineItem(
|
|
||||||
eventId = eventId,
|
|
||||||
transactionId = transactionId,
|
|
||||||
isEditable = isEditable,
|
|
||||||
isLocal = isLocal,
|
|
||||||
isOwn = isOwn,
|
|
||||||
isRemote = isRemote,
|
|
||||||
localSendState = localSendState,
|
|
||||||
reactions = reactions,
|
|
||||||
receipts = receipts,
|
|
||||||
sender = sender,
|
|
||||||
senderProfile = senderProfile,
|
|
||||||
timestamp = timestamp,
|
|
||||||
content = content,
|
|
||||||
debugInfo = debugInfo,
|
|
||||||
origin = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun aProfileTimelineDetails(
|
|
||||||
displayName: String? = A_USER_NAME,
|
|
||||||
displayNameAmbiguous: Boolean = false,
|
|
||||||
avatarUrl: String? = null
|
|
||||||
): ProfileTimelineDetails = ProfileTimelineDetails.Ready(
|
|
||||||
displayName = displayName,
|
|
||||||
displayNameAmbiguous = displayNameAmbiguous,
|
|
||||||
avatarUrl = avatarUrl,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun aProfileChangeMessageContent(
|
|
||||||
displayName: String? = null,
|
|
||||||
prevDisplayName: String? = null,
|
|
||||||
avatarUrl: String? = null,
|
|
||||||
prevAvatarUrl: String? = null,
|
|
||||||
) = ProfileChangeContent(
|
|
||||||
displayName = displayName,
|
|
||||||
prevDisplayName = prevDisplayName,
|
|
||||||
avatarUrl = avatarUrl,
|
|
||||||
prevAvatarUrl = prevAvatarUrl,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun aMessageContent(
|
|
||||||
body: String = "body",
|
|
||||||
inReplyTo: InReplyTo? = null,
|
|
||||||
isEdited: Boolean = false,
|
|
||||||
isThreaded: Boolean = false,
|
|
||||||
messageType: MessageType = TextMessageType(
|
|
||||||
body = body,
|
|
||||||
formatted = null
|
|
||||||
)
|
|
||||||
) = MessageContent(
|
|
||||||
body = body,
|
|
||||||
inReplyTo = inReplyTo,
|
|
||||||
isEdited = isEdited,
|
|
||||||
isThreaded = isThreaded,
|
|
||||||
type = messageType
|
|
||||||
)
|
|
||||||
|
|
||||||
fun aTimelineItemDebugInfo(
|
|
||||||
model: String = "Rust(Model())",
|
|
||||||
originalJson: String? = null,
|
|
||||||
latestEditedJson: String? = null,
|
|
||||||
) = TimelineItemDebugInfo(
|
|
||||||
model, originalJson, latestEditedJson
|
|
||||||
)
|
|
||||||
|
|
||||||
fun aPollContent(
|
|
||||||
question: String = "Do you like polls?",
|
|
||||||
answers: ImmutableList<PollAnswer> = persistentListOf(PollAnswer("1", "Yes"), PollAnswer("2", "No")),
|
|
||||||
) = PollContent(
|
|
||||||
question = question,
|
|
||||||
kind = PollKind.Disclosed,
|
|
||||||
maxSelections = 1u,
|
|
||||||
answers = answers,
|
|
||||||
votes = persistentMapOf(),
|
|
||||||
endTime = null,
|
|
||||||
isEdited = false,
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,10 @@ class FakeMatrixTimeline(
|
||||||
|
|
||||||
override val timelineItems: Flow<List<MatrixTimelineItem>> = _timelineItems
|
override val timelineItems: Flow<List<MatrixTimelineItem>> = _timelineItems
|
||||||
|
|
||||||
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> {
|
override suspend fun paginateBackwards(requestSize: Int) = paginateBackwards()
|
||||||
|
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int) = paginateBackwards()
|
||||||
|
|
||||||
|
private suspend fun paginateBackwards(): Result<Unit> {
|
||||||
updatePaginationState {
|
updatePaginationState {
|
||||||
copy(isBackPaginating = true)
|
copy(isBackPaginating = true)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
/*
|
||||||
|
* 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.test.timeline
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||||
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
|
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.PollContent
|
||||||
|
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.Receipt
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.ImmutableMap
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
|
|
||||||
|
fun anEventTimelineItem(
|
||||||
|
eventId: EventId = AN_EVENT_ID,
|
||||||
|
transactionId: TransactionId? = null,
|
||||||
|
isEditable: Boolean = false,
|
||||||
|
isLocal: Boolean = false,
|
||||||
|
isOwn: Boolean = false,
|
||||||
|
isRemote: Boolean = false,
|
||||||
|
localSendState: LocalEventSendState? = null,
|
||||||
|
reactions: ImmutableList<EventReaction> = persistentListOf(),
|
||||||
|
receipts: ImmutableList<Receipt> = persistentListOf(),
|
||||||
|
sender: UserId = A_USER_ID,
|
||||||
|
senderProfile: ProfileTimelineDetails = aProfileTimelineDetails(),
|
||||||
|
timestamp: Long = 0L,
|
||||||
|
content: EventContent = aProfileChangeMessageContent(),
|
||||||
|
debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(),
|
||||||
|
) = EventTimelineItem(
|
||||||
|
eventId = eventId,
|
||||||
|
transactionId = transactionId,
|
||||||
|
isEditable = isEditable,
|
||||||
|
isLocal = isLocal,
|
||||||
|
isOwn = isOwn,
|
||||||
|
isRemote = isRemote,
|
||||||
|
localSendState = localSendState,
|
||||||
|
reactions = reactions,
|
||||||
|
receipts = receipts,
|
||||||
|
sender = sender,
|
||||||
|
senderProfile = senderProfile,
|
||||||
|
timestamp = timestamp,
|
||||||
|
content = content,
|
||||||
|
debugInfo = debugInfo,
|
||||||
|
origin = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aProfileTimelineDetails(
|
||||||
|
displayName: String? = A_USER_NAME,
|
||||||
|
displayNameAmbiguous: Boolean = false,
|
||||||
|
avatarUrl: String? = null
|
||||||
|
): ProfileTimelineDetails = ProfileTimelineDetails.Ready(
|
||||||
|
displayName = displayName,
|
||||||
|
displayNameAmbiguous = displayNameAmbiguous,
|
||||||
|
avatarUrl = avatarUrl,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aProfileChangeMessageContent(
|
||||||
|
displayName: String? = null,
|
||||||
|
prevDisplayName: String? = null,
|
||||||
|
avatarUrl: String? = null,
|
||||||
|
prevAvatarUrl: String? = null,
|
||||||
|
) = ProfileChangeContent(
|
||||||
|
displayName = displayName,
|
||||||
|
prevDisplayName = prevDisplayName,
|
||||||
|
avatarUrl = avatarUrl,
|
||||||
|
prevAvatarUrl = prevAvatarUrl,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aMessageContent(
|
||||||
|
body: String = "body",
|
||||||
|
inReplyTo: InReplyTo? = null,
|
||||||
|
isEdited: Boolean = false,
|
||||||
|
isThreaded: Boolean = false,
|
||||||
|
messageType: MessageType = TextMessageType(
|
||||||
|
body = body,
|
||||||
|
formatted = null
|
||||||
|
)
|
||||||
|
) = MessageContent(
|
||||||
|
body = body,
|
||||||
|
inReplyTo = inReplyTo,
|
||||||
|
isEdited = isEdited,
|
||||||
|
isThreaded = isThreaded,
|
||||||
|
type = messageType
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aTimelineItemDebugInfo(
|
||||||
|
model: String = "Rust(Model())",
|
||||||
|
originalJson: String? = null,
|
||||||
|
latestEditedJson: String? = null,
|
||||||
|
) = TimelineItemDebugInfo(
|
||||||
|
model, originalJson, latestEditedJson
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aPollContent(
|
||||||
|
question: String = "Do you like polls?",
|
||||||
|
answers: ImmutableList<PollAnswer> = persistentListOf(PollAnswer("1", "Yes"), PollAnswer("2", "No")),
|
||||||
|
kind: PollKind = PollKind.Disclosed,
|
||||||
|
maxSelections: ULong = 1u,
|
||||||
|
votes: ImmutableMap<String, ImmutableList<UserId>> = persistentMapOf(),
|
||||||
|
endTime: ULong? = null,
|
||||||
|
isEdited: Boolean = false,
|
||||||
|
) = PollContent(
|
||||||
|
question = question,
|
||||||
|
kind = kind,
|
||||||
|
maxSelections = maxSelections,
|
||||||
|
answers = answers,
|
||||||
|
votes = votes,
|
||||||
|
endTime = endTime,
|
||||||
|
isEdited = isEdited,
|
||||||
|
)
|
||||||
|
|
@ -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.libraries.matrix.ui.room
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MatrixRoom.rememberPollHistory(): MatrixTimeline {
|
||||||
|
val pollHistory = remember {
|
||||||
|
pollHistory()
|
||||||
|
}
|
||||||
|
DisposableEffect(pollHistory) {
|
||||||
|
onDispose {
|
||||||
|
pollHistory.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pollHistory
|
||||||
|
}
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:ea0b049a734767b1d68f75f808e398357772f37e80a8ad866280c74fc4671251
|
|
||||||
size 54035
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:e2551e11572c82720ddde2909c03d00d3503dadbd0d26e5525cdf9801d61ccc9
|
|
||||||
size 50371
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:cb71e2b69f31bdc3ee7bf17175f099c3838e311978bfed78ce40e6ad98c871e1
|
||||||
|
size 51991
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e7ff7a9241609b8990ad3353947e550607eca4d8809c0ace796a3cd3dd2ff079
|
||||||
|
size 48382
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue