Render the details of the poll

This commit is contained in:
Maxime NATUREL 2023-02-01 17:44:48 +01:00
parent afe036dd9d
commit d3df58c607
14 changed files with 241 additions and 88 deletions

View file

@ -85,6 +85,7 @@ import im.vector.app.features.roomprofile.members.RoomMemberListViewModel
import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsViewModel
import im.vector.app.features.roomprofile.permissions.RoomPermissionsViewModel
import im.vector.app.features.roomprofile.polls.RoomPollsViewModel
import im.vector.app.features.roomprofile.polls.detail.ui.RoomPollDetailViewModel
import im.vector.app.features.roomprofile.settings.RoomSettingsViewModel
import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedViewModel
import im.vector.app.features.roomprofile.uploads.RoomUploadsViewModel
@ -703,4 +704,9 @@ interface MavericksViewModelModule {
@IntoMap
@MavericksViewModelKey(RoomPollsViewModel::class)
fun roomPollsViewModelFactory(factory: RoomPollsViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(RoomPollDetailViewModel::class)
fun roomPollDetailViewModelFactory(factory: RoomPollDetailViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
}

View file

@ -255,7 +255,11 @@ class MessageItemFactory @Inject constructor(
attributes: AbsMessageItem.Attributes,
isEnded: Boolean,
): PollItem {
val pollViewState = pollItemViewStateFactory.create(pollContent, informationData)
val pollViewState = pollItemViewStateFactory.create(
pollContent = pollContent,
pollResponseData = informationData.pollResponseAggregatedSummary,
isSent = informationData.sendState.isSent(),
)
return PollItem_()
.attributes(attributes)

View file

@ -18,9 +18,8 @@ package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.PollResponseData
import im.vector.app.features.poll.PollViewState
import im.vector.app.features.poll.PollItemViewState
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo
@ -33,27 +32,27 @@ class PollItemViewStateFactory @Inject constructor(
fun create(
pollContent: MessagePollContent,
informationData: MessageInformationData,
): PollViewState {
pollResponseData: PollResponseData?,
isSent: Boolean,
): PollItemViewState {
val pollCreationInfo = pollContent.getBestPollCreationInfo()
val question = pollCreationInfo?.question?.getBestQuestion().orEmpty()
val pollResponseSummary = informationData.pollResponseAggregatedSummary
val totalVotes = pollResponseSummary?.totalVotes ?: 0
val totalVotes = pollResponseData?.totalVotes ?: 0
return when {
!informationData.sendState.isSent() -> {
!isSent -> {
createSendingPollViewState(question, pollCreationInfo)
}
informationData.pollResponseAggregatedSummary?.isClosed.orFalse() -> {
createEndedPollViewState(question, pollCreationInfo, pollResponseSummary, totalVotes)
pollResponseData?.isClosed.orFalse() -> {
createEndedPollViewState(question, pollCreationInfo, pollResponseData, totalVotes)
}
pollContent.getBestPollCreationInfo()?.isUndisclosed().orFalse() -> {
createUndisclosedPollViewState(question, pollCreationInfo, pollResponseSummary)
createUndisclosedPollViewState(question, pollCreationInfo, pollResponseData)
}
informationData.pollResponseAggregatedSummary?.myVote?.isNotEmpty().orFalse() -> {
createVotedPollViewState(question, pollCreationInfo, pollResponseSummary, totalVotes)
pollResponseData?.myVote?.isNotEmpty().orFalse() -> {
createVotedPollViewState(question, pollCreationInfo, pollResponseData, totalVotes)
}
else -> {
createReadyPollViewState(question, pollCreationInfo, totalVotes)
@ -61,8 +60,8 @@ class PollItemViewStateFactory @Inject constructor(
}
}
private fun createSendingPollViewState(question: String, pollCreationInfo: PollCreationInfo?): PollViewState {
return PollViewState(
private fun createSendingPollViewState(question: String, pollCreationInfo: PollCreationInfo?): PollItemViewState {
return PollItemViewState(
question = question,
votesStatus = stringProvider.getString(R.string.poll_no_votes_cast),
canVote = false,
@ -73,51 +72,51 @@ class PollItemViewStateFactory @Inject constructor(
private fun createEndedPollViewState(
question: String,
pollCreationInfo: PollCreationInfo?,
pollResponseSummary: PollResponseData?,
pollResponseData: PollResponseData?,
totalVotes: Int,
): PollViewState {
val totalVotesText = if (pollResponseSummary?.hasEncryptedRelatedEvents.orFalse()) {
): PollItemViewState {
val totalVotesText = if (pollResponseData?.hasEncryptedRelatedEvents.orFalse()) {
stringProvider.getString(R.string.unable_to_decrypt_some_events_in_poll)
} else {
stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, totalVotes, totalVotes)
}
return PollViewState(
return PollItemViewState(
question = question,
votesStatus = totalVotesText,
canVote = false,
optionViewStates = pollOptionViewStateFactory.createPollEndedOptions(pollCreationInfo, pollResponseSummary),
optionViewStates = pollOptionViewStateFactory.createPollEndedOptions(pollCreationInfo, pollResponseData),
)
}
private fun createUndisclosedPollViewState(
question: String,
pollCreationInfo: PollCreationInfo?,
pollResponseSummary: PollResponseData?
): PollViewState {
return PollViewState(
pollResponseData: PollResponseData?
): PollItemViewState {
return PollItemViewState(
question = question,
votesStatus = stringProvider.getString(R.string.poll_undisclosed_not_ended),
canVote = true,
optionViewStates = pollOptionViewStateFactory.createPollUndisclosedOptions(pollCreationInfo, pollResponseSummary),
optionViewStates = pollOptionViewStateFactory.createPollUndisclosedOptions(pollCreationInfo, pollResponseData),
)
}
private fun createVotedPollViewState(
question: String,
pollCreationInfo: PollCreationInfo?,
pollResponseSummary: PollResponseData?,
pollResponseData: PollResponseData?,
totalVotes: Int
): PollViewState {
val totalVotesText = if (pollResponseSummary?.hasEncryptedRelatedEvents.orFalse()) {
): PollItemViewState {
val totalVotesText = if (pollResponseData?.hasEncryptedRelatedEvents.orFalse()) {
stringProvider.getString(R.string.unable_to_decrypt_some_events_in_poll)
} else {
stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, totalVotes, totalVotes)
}
return PollViewState(
return PollItemViewState(
question = question,
votesStatus = totalVotesText,
canVote = true,
optionViewStates = pollOptionViewStateFactory.createPollVotedOptions(pollCreationInfo, pollResponseSummary),
optionViewStates = pollOptionViewStateFactory.createPollVotedOptions(pollCreationInfo, pollResponseData),
)
}
@ -125,13 +124,13 @@ class PollItemViewStateFactory @Inject constructor(
question: String,
pollCreationInfo: PollCreationInfo?,
totalVotes: Int
): PollViewState {
): PollItemViewState {
val totalVotesText = if (totalVotes == 0) {
stringProvider.getString(R.string.poll_no_votes_cast)
} else {
stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, totalVotes, totalVotes)
}
return PollViewState(
return PollItemViewState(
question = question,
votesStatus = totalVotesText,
canVote = true,

View file

@ -18,7 +18,7 @@ package im.vector.app.features.poll
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
data class PollViewState(
data class PollItemViewState(
val question: String,
val votesStatus: String,
val canVote: Boolean,

View file

@ -16,14 +16,9 @@
package im.vector.app.features.roomprofile.polls.detail.ui
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
import im.vector.app.features.poll.PollItemViewState
data class RoomPollDetail(
val eventId: String,
val question: String,
val canVote: Boolean,
val votesStatusSummary: String,
val optionViewStates: List<PollOptionViewState>,
val hasBeenEdited: Boolean,
val isEnded: Boolean,
val pollItemViewState: PollItemViewState,
)

View file

@ -1,23 +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 im.vector.app.features.roomprofile.polls.detail.ui
import im.vector.app.core.platform.VectorViewModelAction
sealed interface RoomPollDetailAction : VectorViewModelAction {
}

View file

@ -17,22 +17,29 @@
package im.vector.app.features.roomprofile.polls.detail.ui
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.features.home.room.detail.timeline.item.PollItem_
import javax.inject.Inject
class RoomPollDetailController @Inject constructor()
: TypedEpoxyController<RoomPollDetailViewState>() {
class RoomPollDetailController @Inject constructor() : TypedEpoxyController<RoomPollDetailViewState>() {
interface Callback {
fun vote(pollId: String, optionId: String)
}
var callback: Callback? = null
override fun buildModels(viewState: RoomPollDetailViewState?) {
viewState ?: return
val pollDetail = viewState?.pollDetail ?: return
val pollItemViewState = pollDetail.pollItemViewState
val host = this
PollItem_()
/*
.eventId(pollSummary.id)
.pollQuestion(pollSummary.title.toEpoxyCharSequence())
.canVote(viewState.canVoteSelectedPoll())
.optionViewStates(pollSummary.optionViewStates)
.ended(viewState.canVoteSelectedPoll().not())
*/
roomPollDetailItem {
id(viewState.pollId)
eventId(viewState.pollId)
question(pollItemViewState.question)
canVote(pollItemViewState.canVote)
votesStatus(pollItemViewState.votesStatus)
optionViewStates(pollItemViewState.optionViewStates.orEmpty())
callback(host.callback)
}
}
}

View file

@ -59,6 +59,7 @@ class RoomPollDetailFragment : VectorBaseFragment<FragmentRoomPollDetailBinding>
roomPollDetailController,
hasFixedSize = true,
)
// TODO setup callback in controller for vote action
}
override fun onDestroyView() {
@ -75,15 +76,13 @@ class RoomPollDetailFragment : VectorBaseFragment<FragmentRoomPollDetailBinding>
setupToolbar(views.roomPollDetailToolbar)
.setTitle(title)
.allowBack()
.allowBack(useCross = true)
}
override fun invalidate() = withState(viewModel) { state ->
state.pollDetail ?: return@withState
// TODO should we update the title when the poll status changes?
setupToolbar(state.pollDetail.isEnded)
// TODO update data of the controller
roomPollDetailController.setData(state)
}
}

View file

@ -0,0 +1,85 @@
/*
* Copyright 2020 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 im.vector.app.features.roomprofile.polls.detail.ui
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.features.home.room.detail.timeline.item.PollOptionView
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
@EpoxyModelClass
abstract class RoomPollDetailItem : VectorEpoxyModel<RoomPollDetailItem.Holder>(R.layout.item_timeline_event_poll) {
@EpoxyAttribute
var question: String? = null
@EpoxyAttribute
var callback: RoomPollDetailController.Callback? = null
@EpoxyAttribute
var eventId: String? = null
@EpoxyAttribute
var canVote: Boolean = false
@EpoxyAttribute
var votesStatus: String? = null
@EpoxyAttribute
lateinit var optionViewStates: List<PollOptionViewState>
@EpoxyAttribute
var ended: Boolean = false
override fun bind(holder: Holder) {
super.bind(holder)
holder.questionTextView.text = question
holder.votesStatusTextView.text = votesStatus
holder.optionsContainer.removeAllViews()
holder.optionsContainer.isVisible = optionViewStates.isNotEmpty()
for (option in optionViewStates) {
val optionView = PollOptionView(holder.view.context)
holder.optionsContainer.addView(optionView)
optionView.render(option)
optionView.setOnClickListener { onOptionClicked(option) }
}
holder.endedPollTextView.isVisible = false
}
private fun onOptionClicked(optionViewState: PollOptionViewState) {
val relatedEventId = eventId
if (canVote && relatedEventId != null) {
callback?.vote(pollId = relatedEventId, optionId = optionViewState.optionId)
}
}
class Holder : VectorEpoxyHolder() {
val questionTextView by bind<TextView>(R.id.questionTextView)
val optionsContainer by bind<LinearLayout>(R.id.optionsContainer)
val votesStatusTextView by bind<TextView>(R.id.optionsVotesStatusTextView)
val endedPollTextView by bind<TextView>(R.id.endedPollTextView)
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.roomprofile.polls.detail.ui
import im.vector.app.core.extensions.getVectorLastMessageContent
import im.vector.app.features.home.room.detail.timeline.factory.PollItemViewStateFactory
import im.vector.app.features.home.room.detail.timeline.helper.PollResponseDataFactory
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import timber.log.Timber
import javax.inject.Inject
// TODO add unit tests
class RoomPollDetailMapper @Inject constructor(
private val pollResponseDataFactory: PollResponseDataFactory,
private val pollItemViewStateFactory: PollItemViewStateFactory,
) {
fun map(timelineEvent: TimelineEvent): RoomPollDetail? {
val eventId = timelineEvent.root.eventId.orEmpty()
val result = runCatching {
val content = timelineEvent.getVectorLastMessageContent()
val pollResponseData = pollResponseDataFactory.create(timelineEvent)
return if (eventId.isNotEmpty() && content is MessagePollContent) {
// we assume poll message has been sent here
val pollItemViewState = pollItemViewStateFactory.create(
pollContent = content,
pollResponseData = pollResponseData,
isSent = true,
)
RoomPollDetail(
isEnded = pollResponseData?.isClosed == true,
pollItemViewState = pollItemViewState,
)
} else {
Timber.w("missing mandatory info about poll event with id=$eventId")
null
}
}
if (result.isFailure) {
Timber.w("failed to map event with id $eventId")
}
return result.getOrNull()
}
}

View file

@ -16,16 +16,33 @@
package im.vector.app.features.roomprofile.polls.detail.ui
import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.event.GetTimelineEventUseCase
import im.vector.app.core.platform.EmptyAction
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.auth.ReAuthState
import im.vector.app.features.auth.ReAuthViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
class RoomPollDetailViewModel @AssistedInject constructor(
@Assisted initialState: RoomPollDetailViewState,
private val getTimelineEventUseCase: GetTimelineEventUseCase,
) : VectorViewModel<RoomPollDetailViewState, RoomPollDetailAction, RoomPollDetailViewEvent>(initialState) {
private val roomPollDetailMapper: RoomPollDetailMapper,
) : VectorViewModel<RoomPollDetailViewState, EmptyAction, RoomPollDetailViewEvent>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<RoomPollDetailViewModel, RoomPollDetailViewState> {
override fun create(initialState: RoomPollDetailViewState): RoomPollDetailViewModel
}
companion object : MavericksViewModelFactory<RoomPollDetailViewModel, RoomPollDetailViewState> by hiltMavericksViewModelFactory()
init {
observePollDetails(
@ -36,10 +53,12 @@ class RoomPollDetailViewModel @AssistedInject constructor(
private fun observePollDetails(pollId: String, roomId: String) {
getTimelineEventUseCase.execute(roomId = roomId, eventId = pollId)
.map { roomPollDetailMapper.map(it) }
.onEach { setState { copy(pollDetail = it) } }
.launchIn(viewModelScope)
}
override fun handle(action: RoomPollDetailAction) {
// TODO handle go to timeline action
override fun handle(action: EmptyAction) {
// do nothing for now
}
}

View file

@ -17,6 +17,7 @@
package im.vector.app.features.roomprofile.polls.detail.ui
import com.airbnb.mvrx.MavericksState
import im.vector.app.features.poll.PollItemViewState
data class RoomPollDetailViewState(
val pollId: String,

View file

@ -24,12 +24,13 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/pollDetailRecyclerView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_height="0dp"
android:layout_marginTop="32dp"
android:paddingHorizontal="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appBarLayout"
app:layout_constraintBottom_toBottomOf="parent"
tools:itemCount="1"
tools:listitem="@layout/item_timeline_event_poll" />

View file

@ -19,7 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.R
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
import im.vector.app.features.home.room.detail.timeline.item.PollVoteSummaryData
import im.vector.app.features.poll.PollViewState
import im.vector.app.features.poll.PollItemViewState
import im.vector.app.test.fakes.FakeStringProvider
import im.vector.app.test.fixtures.PollFixture.A_MESSAGE_INFORMATION_DATA
import im.vector.app.test.fixtures.PollFixture.A_POLL_CONTENT
@ -57,7 +57,7 @@ class PollItemViewStateFactoryTest {
)
// Then
pollViewState shouldBeEqualTo PollViewState(
pollViewState shouldBeEqualTo PollItemViewState(
question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "",
votesStatus = fakeStringProvider.instance.getString(R.string.poll_no_votes_cast),
canVote = false,
@ -90,7 +90,7 @@ class PollItemViewStateFactoryTest {
)
// Then
pollViewState shouldBeEqualTo PollViewState(
pollViewState shouldBeEqualTo PollItemViewState(
question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "",
votesStatus = fakeStringProvider.instance.getQuantityString(R.plurals.poll_total_vote_count_after_ended, 0, 0),
canVote = false,
@ -155,7 +155,7 @@ class PollItemViewStateFactoryTest {
)
// Then
pollViewState shouldBeEqualTo PollViewState(
pollViewState shouldBeEqualTo PollItemViewState(
question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "",
votesStatus = fakeStringProvider.instance.getString(R.string.poll_undisclosed_not_ended),
canVote = true,
@ -204,7 +204,7 @@ class PollItemViewStateFactoryTest {
)
// Then
pollViewState shouldBeEqualTo PollViewState(
pollViewState shouldBeEqualTo PollItemViewState(
question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "",
votesStatus = fakeStringProvider.instance.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, 1, 1),
canVote = true,
@ -286,7 +286,7 @@ class PollItemViewStateFactoryTest {
)
// Then
pollViewState shouldBeEqualTo PollViewState(
pollViewState shouldBeEqualTo PollItemViewState(
question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "",
votesStatus = fakeStringProvider.instance.getString(R.string.poll_no_votes_cast),
canVote = true,