mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-21 17:05:39 +03:00
Merge pull request #8069 from vector-im/feature/mna/poll-history-details
[Poll] History list: details screen of a poll (PSG-1041, PSG-1151)
This commit is contained in:
commit
552af673ff
42 changed files with 1929 additions and 134 deletions
2
changelog.d/8056.feature
Normal file
2
changelog.d/8056.feature
Normal file
|
@ -0,0 +1,2 @@
|
|||
[Poll] History list: details screen of a poll
|
||||
[Poll] History list: enable the new settings entry in release mode
|
|
@ -3211,6 +3211,7 @@
|
|||
<string name="room_polls_wait_for_display">Displaying polls</string>
|
||||
<string name="room_polls_load_more">Load more polls</string>
|
||||
<string name="room_polls_loading_error">Error fetching polls.</string>
|
||||
<string name="room_poll_details_go_to_timeline">View poll in timeline</string>
|
||||
|
||||
<!-- Location -->
|
||||
<string name="location_activity_title_static_sharing">Share location</string>
|
||||
|
|
|
@ -327,6 +327,7 @@
|
|||
<activity android:name=".features.settings.devices.v2.details.SessionDetailsActivity" />
|
||||
<activity android:name=".features.settings.devices.v2.rename.RenameSessionActivity" />
|
||||
<activity android:name=".features.login.qr.QrCodeLoginActivity" />
|
||||
<activity android:name=".features.roomprofile.polls.detail.ui.RoomPollDetailActivity" />
|
||||
|
||||
<!-- Services -->
|
||||
|
||||
|
|
|
@ -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<*, *>
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.core.event
|
||||
|
||||
import androidx.lifecycle.asFlow
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.flow.unwrap
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetTimelineEventUseCase @Inject constructor(
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
) {
|
||||
|
||||
fun execute(roomId: String, eventId: String): Flow<TimelineEvent> {
|
||||
return activeSessionHolder.getActiveSession().getRoom(roomId)
|
||||
?.timelineService()
|
||||
?.getTimelineEventLive(eventId)
|
||||
?.asFlow()
|
||||
?.unwrap()
|
||||
?: emptyFlow()
|
||||
}
|
||||
}
|
|
@ -54,6 +54,7 @@ import im.vector.app.features.crypto.verification.SupportedVerificationMethodsPr
|
|||
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
|
||||
import im.vector.app.features.home.room.detail.error.RoomNotFound
|
||||
import im.vector.app.features.home.room.detail.location.RedactLiveLocationShareEventUseCase
|
||||
import im.vector.app.features.home.room.detail.poll.VoteToPollUseCase
|
||||
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
|
||||
import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||
|
@ -90,7 +91,6 @@ import org.matrix.android.sdk.api.raw.RawService
|
|||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.LocalEcho
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
|
||||
|
@ -154,6 +154,7 @@ class TimelineViewModel @AssistedInject constructor(
|
|||
timelineFactory: TimelineFactory,
|
||||
private val spaceStateHandler: SpaceStateHandler,
|
||||
private val voiceBroadcastHelper: VoiceBroadcastHelper,
|
||||
private val voteToPollUseCase: VoteToPollUseCase,
|
||||
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
|
||||
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener, LocationSharingServiceConnection.Callback {
|
||||
|
||||
|
@ -1235,15 +1236,11 @@ class TimelineViewModel @AssistedInject constructor(
|
|||
|
||||
private fun handleVoteToPoll(action: RoomDetailAction.VoteToPoll) {
|
||||
if (room == null) return
|
||||
// Do not allow to vote unsent local echo of the poll event
|
||||
if (LocalEcho.isLocalEchoId(action.eventId)) return
|
||||
// Do not allow to vote the same option twice
|
||||
room.getTimelineEvent(action.eventId)?.let { pollTimelineEvent ->
|
||||
val currentVote = pollTimelineEvent.annotations?.pollResponseSummary?.aggregatedContent?.myVote
|
||||
if (currentVote != action.optionKey) {
|
||||
room.sendService().voteToPoll(action.eventId, action.optionKey)
|
||||
}
|
||||
}
|
||||
voteToPollUseCase.execute(
|
||||
roomId = room.roomId,
|
||||
pollEventId = action.eventId,
|
||||
optionId = action.optionKey,
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleEndPoll(eventId: String) {
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.poll
|
||||
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import org.matrix.android.sdk.api.session.events.model.LocalEcho
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class VoteToPollUseCase @Inject constructor(
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
) {
|
||||
|
||||
fun execute(roomId: String, pollEventId: String, optionId: String) {
|
||||
// Do not allow to vote unsent local echo of the poll event
|
||||
if (LocalEcho.isLocalEchoId(pollEventId)) return
|
||||
|
||||
runCatching {
|
||||
val room = activeSessionHolder.getActiveSession().getRoom(roomId)
|
||||
room?.getTimelineEvent(pollEventId)?.let { pollTimelineEvent ->
|
||||
val currentVote = pollTimelineEvent
|
||||
.annotations
|
||||
?.pollResponseSummary
|
||||
?.aggregatedContent
|
||||
?.myVote
|
||||
if (currentVote != optionId) {
|
||||
room.sendService().voteToPoll(
|
||||
pollEventId = pollEventId,
|
||||
answerId = optionId
|
||||
)
|
||||
}
|
||||
}
|
||||
}.onFailure { Timber.w("Failed to vote in poll with id $pollEventId in room with id $roomId") }
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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,25 @@ 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 +58,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 +70,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 +122,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,
|
||||
|
|
|
@ -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,
|
|
@ -18,7 +18,6 @@
|
|||
package im.vector.app.features.roomprofile
|
||||
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.expandableTextItem
|
||||
import im.vector.app.core.epoxy.profiles.buildProfileAction
|
||||
|
@ -265,15 +264,14 @@ class RoomProfileController @Inject constructor(
|
|||
action = { callback?.onBannedMemberListClicked() }
|
||||
)
|
||||
}
|
||||
if (BuildConfig.DEBUG) {
|
||||
// WIP, will be in release when related screens will be finished
|
||||
buildProfileAction(
|
||||
id = "poll_history",
|
||||
title = stringProvider.getString(R.string.room_profile_section_more_polls),
|
||||
icon = R.drawable.ic_attachment_poll,
|
||||
action = { callback?.onPollHistoryClicked() }
|
||||
)
|
||||
}
|
||||
|
||||
buildProfileAction(
|
||||
id = "poll_history",
|
||||
title = stringProvider.getString(R.string.room_profile_section_more_polls),
|
||||
icon = R.drawable.ic_attachment_poll,
|
||||
action = { callback?.onPollHistoryClicked() }
|
||||
)
|
||||
|
||||
buildProfileAction(
|
||||
id = "uploads",
|
||||
title = stringProvider.getString(R.string.room_profile_section_more_uploads),
|
||||
|
|
|
@ -64,7 +64,7 @@ import javax.inject.Inject
|
|||
|
||||
@Parcelize
|
||||
data class RoomProfileArgs(
|
||||
val roomId: String
|
||||
val roomId: String,
|
||||
) : Parcelable
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
|
|
@ -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 im.vector.app.features.roomprofile.polls.detail.domain
|
||||
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.isPollEnd
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetEndedPollEventIdUseCase @Inject constructor(
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
) {
|
||||
|
||||
fun execute(roomId: String, startPollEventId: String): String? {
|
||||
val result = runCatching {
|
||||
activeSessionHolder.getActiveSession().roomService().getRoom(roomId)
|
||||
?.timelineService()
|
||||
?.getTimelineEventsRelatedTo(RelationType.REFERENCE, startPollEventId)
|
||||
?.find { it.root.isPollEnd() }
|
||||
?.eventId
|
||||
}.onFailure { Timber.w("failed to retrieve the ended poll event id for eventId:$startPollEventId") }
|
||||
return result.getOrNull()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.features.poll.PollItemViewState
|
||||
|
||||
data class RoomPollDetail(
|
||||
val creationTimestamp: Long,
|
||||
val isEnded: Boolean,
|
||||
val endedPollEventId: String?,
|
||||
val pollItemViewState: PollItemViewState,
|
||||
)
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.roomprofile.polls.detail.ui
|
||||
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
|
||||
sealed interface RoomPollDetailAction : VectorViewModelAction {
|
||||
data class Vote(val pollEventId: String, val optionId: String) : RoomPollDetailAction
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import com.airbnb.mvrx.Mavericks
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.core.extensions.addFragment
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.databinding.ActivitySimpleBinding
|
||||
import im.vector.lib.core.utils.compat.getParcelableExtraCompat
|
||||
|
||||
/**
|
||||
* Display the details of a given poll.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class RoomPollDetailActivity : VectorBaseActivity<ActivitySimpleBinding>() {
|
||||
|
||||
override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (isFirstCreation()) {
|
||||
addFragment(
|
||||
container = views.simpleFragmentContainer,
|
||||
fragmentClass = RoomPollDetailFragment::class.java,
|
||||
params = intent.getParcelableExtraCompat(Mavericks.KEY_ARG)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newIntent(context: Context, pollId: String, roomId: String, isEnded: Boolean): Intent {
|
||||
return Intent(context, RoomPollDetailActivity::class.java).apply {
|
||||
val args = RoomPollDetailArgs(
|
||||
pollId = pollId,
|
||||
roomId = roomId,
|
||||
isEnded = isEnded,
|
||||
)
|
||||
putExtra(Mavericks.KEY_ARG, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 com.airbnb.epoxy.TypedEpoxyController
|
||||
import im.vector.app.core.date.DateFormatKind
|
||||
import im.vector.app.core.date.VectorDateFormatter
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomPollDetailController @Inject constructor(
|
||||
val dateFormatter: VectorDateFormatter,
|
||||
) : TypedEpoxyController<RoomPollDetailViewState>() {
|
||||
|
||||
interface Callback {
|
||||
fun vote(pollEventId: String, optionId: String)
|
||||
fun goToTimelineEvent(eventId: String)
|
||||
}
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
override fun buildModels(viewState: RoomPollDetailViewState?) {
|
||||
val pollDetail = viewState?.pollDetail ?: return
|
||||
val pollItemViewState = pollDetail.pollItemViewState
|
||||
val host = this
|
||||
|
||||
roomPollDetailItem {
|
||||
id(viewState.pollId)
|
||||
eventId(viewState.pollId)
|
||||
formattedDate(host.dateFormatter.format(pollDetail.creationTimestamp, DateFormatKind.TIMELINE_DAY_DIVIDER))
|
||||
question(pollItemViewState.question)
|
||||
canVote(pollItemViewState.canVote)
|
||||
votesStatus(pollItemViewState.votesStatus)
|
||||
optionViewStates(pollItemViewState.optionViewStates.orEmpty())
|
||||
callback(host.callback)
|
||||
}
|
||||
|
||||
buildGoToTimelineItem(targetEventId = pollDetail.endedPollEventId ?: viewState.pollId)
|
||||
}
|
||||
|
||||
private fun buildGoToTimelineItem(targetEventId: String) {
|
||||
val host = this
|
||||
roomPollGoToTimelineItem {
|
||||
id(UUID.randomUUID().toString())
|
||||
clickListener {
|
||||
host.callback?.goToTimelineEvent(targetEventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.airbnb.mvrx.args
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.cleanup
|
||||
import im.vector.app.core.extensions.configureWith
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.databinding.FragmentRoomPollDetailBinding
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
@Parcelize
|
||||
data class RoomPollDetailArgs(
|
||||
val pollId: String,
|
||||
val roomId: String,
|
||||
val isEnded: Boolean,
|
||||
) : Parcelable
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RoomPollDetailFragment :
|
||||
VectorBaseFragment<FragmentRoomPollDetailBinding>(),
|
||||
RoomPollDetailController.Callback {
|
||||
|
||||
@Inject lateinit var viewNavigator: RoomPollDetailNavigator
|
||||
@Inject lateinit var roomPollDetailController: RoomPollDetailController
|
||||
|
||||
private val viewModel: RoomPollDetailViewModel by fragmentViewModel()
|
||||
private val roomPollDetailArgs: RoomPollDetailArgs by args()
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomPollDetailBinding {
|
||||
return FragmentRoomPollDetailBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupToolbar(isEnded = roomPollDetailArgs.isEnded)
|
||||
setupDetailView()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
roomPollDetailController.callback = null
|
||||
views.pollDetailRecyclerView.cleanup()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setupDetailView() {
|
||||
roomPollDetailController.callback = this
|
||||
views.pollDetailRecyclerView.configureWith(
|
||||
roomPollDetailController,
|
||||
hasFixedSize = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupToolbar(isEnded: Boolean) {
|
||||
val title = when (isEnded) {
|
||||
true -> getString(R.string.room_polls_ended)
|
||||
false -> getString(R.string.room_polls_active)
|
||||
}
|
||||
|
||||
setupToolbar(views.roomPollDetailToolbar)
|
||||
.setTitle(title)
|
||||
.allowBack(useCross = true)
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
roomPollDetailController.setData(state)
|
||||
}
|
||||
|
||||
override fun vote(pollEventId: String, optionId: String) {
|
||||
viewModel.handle(RoomPollDetailAction.Vote(pollEventId = pollEventId, optionId = optionId))
|
||||
}
|
||||
|
||||
override fun goToTimelineEvent(eventId: String) = withState(viewModel) { state ->
|
||||
viewNavigator.goToTimelineEvent(
|
||||
context = requireContext(),
|
||||
roomId = state.roomId,
|
||||
eventId = eventId,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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_poll_detail) {
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var formattedDate: String
|
||||
|
||||
@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.date.text = formattedDate
|
||||
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(pollEventId = relatedEventId, optionId = optionViewState.optionId)
|
||||
}
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val date by bind<TextView>(R.id.pollDetailDate)
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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 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 im.vector.app.features.home.room.detail.timeline.item.PollResponseData
|
||||
import im.vector.app.features.roomprofile.polls.detail.domain.GetEndedPollEventIdUseCase
|
||||
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.timeline.TimelineEvent
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomPollDetailMapper @Inject constructor(
|
||||
private val pollResponseDataFactory: PollResponseDataFactory,
|
||||
private val pollItemViewStateFactory: PollItemViewStateFactory,
|
||||
private val getEndedPollEventIdUseCase: GetEndedPollEventIdUseCase,
|
||||
) {
|
||||
|
||||
fun map(timelineEvent: TimelineEvent): RoomPollDetail? {
|
||||
val eventId = timelineEvent.root.eventId.orEmpty()
|
||||
val result = runCatching {
|
||||
val content = timelineEvent.getVectorLastMessageContent()
|
||||
val pollResponseData = pollResponseDataFactory.create(timelineEvent)
|
||||
val creationTimestamp = timelineEvent.root.originServerTs ?: 0
|
||||
return if (eventId.isNotEmpty() && creationTimestamp > 0 && content is MessagePollContent) {
|
||||
val isPollEnded = pollResponseData?.isClosed.orFalse()
|
||||
val endedPollEventId = getEndedPollEventId(
|
||||
isPollEnded,
|
||||
startPollEventId = eventId,
|
||||
roomId = timelineEvent.roomId,
|
||||
)
|
||||
convertToRoomPollDetail(
|
||||
creationTimestamp = creationTimestamp,
|
||||
content = content,
|
||||
pollResponseData = pollResponseData,
|
||||
isPollEnded = isPollEnded,
|
||||
endedPollEventId = endedPollEventId,
|
||||
)
|
||||
} 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()
|
||||
}
|
||||
|
||||
private fun convertToRoomPollDetail(
|
||||
creationTimestamp: Long,
|
||||
content: MessagePollContent,
|
||||
pollResponseData: PollResponseData?,
|
||||
isPollEnded: Boolean,
|
||||
endedPollEventId: String?,
|
||||
): RoomPollDetail {
|
||||
// we assume the poll has been sent
|
||||
val pollItemViewState = pollItemViewStateFactory.create(
|
||||
pollContent = content,
|
||||
pollResponseData = pollResponseData,
|
||||
isSent = true,
|
||||
)
|
||||
return RoomPollDetail(
|
||||
creationTimestamp = creationTimestamp,
|
||||
isEnded = isPollEnded,
|
||||
pollItemViewState = pollItemViewState,
|
||||
endedPollEventId = endedPollEventId,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getEndedPollEventId(
|
||||
isPollEnded: Boolean,
|
||||
startPollEventId: String,
|
||||
roomId: String,
|
||||
): String? {
|
||||
return if (isPollEnded) {
|
||||
getEndedPollEventIdUseCase.execute(startPollEventId = startPollEventId, roomId = roomId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.roomprofile.polls.detail.ui
|
||||
|
||||
import android.content.Context
|
||||
import im.vector.app.features.navigation.Navigator
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomPollDetailNavigator @Inject constructor(
|
||||
private val navigator: Navigator,
|
||||
) {
|
||||
|
||||
fun goToTimelineEvent(context: Context, roomId: String, eventId: String) {
|
||||
navigator.openRoom(
|
||||
context = context,
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
buildTask = true,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 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.EmptyViewEvents
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.features.home.room.detail.poll.VoteToPollUseCase
|
||||
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,
|
||||
private val roomPollDetailMapper: RoomPollDetailMapper,
|
||||
private val voteToPollUseCase: VoteToPollUseCase,
|
||||
) : VectorViewModel<RoomPollDetailViewState, RoomPollDetailAction, EmptyViewEvents>(initialState) {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MavericksAssistedViewModelFactory<RoomPollDetailViewModel, RoomPollDetailViewState> {
|
||||
override fun create(initialState: RoomPollDetailViewState): RoomPollDetailViewModel
|
||||
}
|
||||
|
||||
companion object : MavericksViewModelFactory<RoomPollDetailViewModel, RoomPollDetailViewState> by hiltMavericksViewModelFactory()
|
||||
|
||||
init {
|
||||
observePollDetails(
|
||||
pollId = initialState.pollId,
|
||||
roomId = initialState.roomId,
|
||||
)
|
||||
}
|
||||
|
||||
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) {
|
||||
when (action) {
|
||||
is RoomPollDetailAction.Vote -> handleVote(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleVote(vote: RoomPollDetailAction.Vote) = withState { state ->
|
||||
voteToPollUseCase.execute(
|
||||
roomId = state.roomId,
|
||||
pollEventId = vote.pollEventId,
|
||||
optionId = vote.optionId,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 com.airbnb.mvrx.MavericksState
|
||||
|
||||
data class RoomPollDetailViewState(
|
||||
val pollId: String,
|
||||
val roomId: String,
|
||||
val pollDetail: RoomPollDetail? = null,
|
||||
) : MavericksState {
|
||||
|
||||
constructor(roomPollDetailArgs: RoomPollDetailArgs) : this(
|
||||
pollId = roomPollDetailArgs.pollId,
|
||||
roomId = roomPollDetailArgs.roomId,
|
||||
)
|
||||
}
|
|
@ -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 im.vector.app.features.roomprofile.polls.detail.ui
|
||||
|
||||
import android.widget.Button
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.ClickListener
|
||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
|
||||
@EpoxyModelClass
|
||||
abstract class RoomPollGoToTimelineItem : VectorEpoxyModel<RoomPollGoToTimelineItem.Holder>(R.layout.item_poll_go_to_timeline) {
|
||||
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
var clickListener: ClickListener? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
holder.goToTimelineButton.onClick(clickListener)
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val goToTimelineButton by bind<Button>(R.id.roomPollGoToTimeline)
|
||||
}
|
||||
}
|
|
@ -69,7 +69,9 @@ class PollSummaryMapper @Inject constructor(
|
|||
creationTimestamp = creationTimestamp,
|
||||
title = pollTitle,
|
||||
totalVotes = pollResponseData.totalVotes,
|
||||
winnerOptions = pollOptionViewStateFactory.createPollEndedOptions(pollCreationInfo, pollResponseData)
|
||||
winnerOptions = pollOptionViewStateFactory
|
||||
.createPollEndedOptions(pollCreationInfo, pollResponseData)
|
||||
.filter { it.isWinner },
|
||||
)
|
||||
} else {
|
||||
PollSummary.ActivePoll(
|
||||
|
|
|
@ -35,7 +35,6 @@ import im.vector.app.features.roomprofile.polls.RoomPollsType
|
|||
import im.vector.app.features.roomprofile.polls.RoomPollsViewEvent
|
||||
import im.vector.app.features.roomprofile.polls.RoomPollsViewModel
|
||||
import im.vector.app.features.roomprofile.polls.RoomPollsViewState
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
abstract class RoomPollsListFragment :
|
||||
|
@ -48,6 +47,9 @@ abstract class RoomPollsListFragment :
|
|||
@Inject
|
||||
lateinit var stringProvider: StringProvider
|
||||
|
||||
@Inject
|
||||
lateinit var viewNavigator: RoomPollsListNavigator
|
||||
|
||||
private val viewModel: RoomPollsViewModel by parentFragmentViewModel(RoomPollsViewModel::class)
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomPollsListBinding {
|
||||
|
@ -125,9 +127,13 @@ abstract class RoomPollsListFragment :
|
|||
views.roomPollsLoadMoreWhenEmptyProgress.isVisible = viewState.hasNoPollsAndCanLoadMore() && viewState.isLoadingMore
|
||||
}
|
||||
|
||||
override fun onPollClicked(pollId: String) {
|
||||
// TODO navigate to details
|
||||
Timber.d("poll with id $pollId clicked")
|
||||
override fun onPollClicked(pollId: String) = withState(viewModel) {
|
||||
viewNavigator.goToPollDetails(
|
||||
context = requireContext(),
|
||||
pollId = pollId,
|
||||
roomId = it.roomId,
|
||||
isEnded = getRoomPollsType() == RoomPollsType.ENDED,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onLoadMoreClicked() {
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.roomprofile.polls.list.ui
|
||||
|
||||
import android.content.Context
|
||||
import im.vector.app.features.roomprofile.polls.detail.ui.RoomPollDetailActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomPollsListNavigator @Inject constructor() {
|
||||
|
||||
fun goToPollDetails(context: Context, pollId: String, roomId: String, isEnded: Boolean) {
|
||||
context.startActivity(
|
||||
RoomPollDetailActivity.newIntent(
|
||||
context = context,
|
||||
pollId = pollId,
|
||||
roomId = roomId,
|
||||
isEnded = isEnded,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
37
vector/src/main/res/layout/fragment_room_poll_detail.xml
Normal file
37
vector/src/main/res/layout/fragment_room_poll_detail.xml
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appBarLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/roomPollDetailToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?actionBarSize"
|
||||
tools:title="@string/room_polls_active" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/pollDetailRecyclerView"
|
||||
android:layout_width="0dp"
|
||||
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" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
29
vector/src/main/res/layout/item_poll_detail.xml
Normal file
29
vector/src/main/res/layout/item_poll_detail.xml
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pollDetailDate"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Caption"
|
||||
android:textColor="?vctr_content_tertiary"
|
||||
app:layout_constraintBottom_toTopOf="@id/pollDetailContent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="28/06/22" />
|
||||
|
||||
<include
|
||||
android:id="@+id/pollDetailContent"
|
||||
layout="@layout/item_timeline_event_poll"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/pollDetailDate" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
20
vector/src/main/res/layout/item_poll_go_to_timeline.xml
Normal file
20
vector/src/main/res/layout/item_poll_go_to_timeline.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<Button
|
||||
android:id="@+id/roomPollGoToTimeline"
|
||||
style="@style/Widget.Vector.Button.Text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="33dp"
|
||||
android:layout_marginBottom="46dp"
|
||||
android:padding="0dp"
|
||||
android:text="@string/room_poll_details_go_to_timeline"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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.core.event
|
||||
|
||||
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
||||
import im.vector.app.test.fakes.FakeFlowLiveDataConversions
|
||||
import im.vector.app.test.fakes.givenAsFlow
|
||||
import im.vector.app.test.test
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.unmockkAll
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.shouldBeNull
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
|
||||
private const val A_ROOM_ID = "room-id"
|
||||
private const val AN_EVENT_ID = "event-id"
|
||||
|
||||
internal class GetTimelineEventUseCaseTest {
|
||||
|
||||
private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions()
|
||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
||||
private val getTimelineEventUseCase = GetTimelineEventUseCase(fakeActiveSessionHolder.instance)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
fakeFlowLiveDataConversions.setup()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a non existing room id, when execute, then returns an empty flow`() = runTest {
|
||||
// Given
|
||||
every { fakeActiveSessionHolder.instance.getActiveSession().roomService().getRoom(A_ROOM_ID) } returns null
|
||||
|
||||
// When
|
||||
val result = getTimelineEventUseCase.execute(A_ROOM_ID, AN_EVENT_ID).firstOrNull()
|
||||
|
||||
// Then
|
||||
result.shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a LiveData of TimelineEvent, when execute, then returns the expected Flow`() = runTest {
|
||||
// Given
|
||||
val aTimelineEvent1: TimelineEvent = mockk()
|
||||
val aTimelineEvent2: TimelineEvent? = null
|
||||
val timelineService = fakeActiveSessionHolder.fakeSession
|
||||
.fakeRoomService
|
||||
.getRoom(A_ROOM_ID)
|
||||
.timelineService()
|
||||
|
||||
// When
|
||||
timelineService.givenTimelineEventLiveReturns(AN_EVENT_ID, aTimelineEvent1).givenAsFlow()
|
||||
val result1 = getTimelineEventUseCase.execute(A_ROOM_ID, AN_EVENT_ID).test(this)
|
||||
|
||||
timelineService.givenTimelineEventLiveReturns(AN_EVENT_ID, aTimelineEvent2).givenAsFlow()
|
||||
val result2 = getTimelineEventUseCase.execute(A_ROOM_ID, AN_EVENT_ID).test(this)
|
||||
|
||||
// Then
|
||||
runCurrent()
|
||||
result1.assertLatestValue(aTimelineEvent1)
|
||||
result2.assertNoValues()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.poll
|
||||
|
||||
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
||||
import im.vector.app.test.fixtures.RoomPollFixture
|
||||
import io.mockk.every
|
||||
import io.mockk.verify
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
||||
|
||||
private const val A_LOCAL_EVENT_ID = "\$local.event-id"
|
||||
private const val AN_EVENT_ID = "event-id"
|
||||
private const val A_ROOM_ID = "room-id"
|
||||
private const val AN_EXISTING_OPTION_ID = "an-existing-option-id"
|
||||
private const val AN_OPTION_ID = "option-id"
|
||||
private const val AN_EVENT_TIMESTAMP = 123L
|
||||
|
||||
internal class VoteToPollUseCaseTest {
|
||||
|
||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
||||
private val voteToPollUseCase = VoteToPollUseCase(fakeActiveSessionHolder.instance)
|
||||
|
||||
@Test
|
||||
fun `given a local echo poll event when voting for an option then the vote is aborted`() {
|
||||
// Given
|
||||
val aPollEventId = A_LOCAL_EVENT_ID
|
||||
val aVoteId = AN_OPTION_ID
|
||||
givenAPollTimelineEvent(aPollEventId)
|
||||
|
||||
// When
|
||||
voteToPollUseCase.execute(A_ROOM_ID, aPollEventId, aVoteId)
|
||||
|
||||
// Then
|
||||
verify(exactly = 0) {
|
||||
fakeActiveSessionHolder.fakeSession
|
||||
.getRoom(A_ROOM_ID)
|
||||
?.sendService()
|
||||
?.voteToPoll(aPollEventId, aVoteId)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a poll event when voting for a different option then the vote is sent`() {
|
||||
// Given
|
||||
val aPollEventId = AN_EVENT_ID
|
||||
val aVoteId = AN_OPTION_ID
|
||||
givenAPollTimelineEvent(aPollEventId)
|
||||
|
||||
// When
|
||||
voteToPollUseCase.execute(A_ROOM_ID, aPollEventId, aVoteId)
|
||||
|
||||
// Then
|
||||
verify(exactly = 1) {
|
||||
fakeActiveSessionHolder.fakeSession
|
||||
.getRoom(A_ROOM_ID)
|
||||
?.sendService()
|
||||
?.voteToPoll(aPollEventId, aVoteId)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a poll event when voting for the same option then the vote is aborted`() {
|
||||
// Given
|
||||
val aPollEventId = AN_EVENT_ID
|
||||
val aVoteId = AN_EXISTING_OPTION_ID
|
||||
givenAPollTimelineEvent(aPollEventId)
|
||||
|
||||
// When
|
||||
voteToPollUseCase.execute(A_ROOM_ID, aPollEventId, aVoteId)
|
||||
|
||||
// Then
|
||||
verify(exactly = 0) {
|
||||
fakeActiveSessionHolder.fakeSession
|
||||
.getRoom(A_ROOM_ID)
|
||||
?.sendService()
|
||||
?.voteToPoll(aPollEventId, aVoteId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun givenAPollTimelineEvent(eventId: String) {
|
||||
val pollCreationInfo = RoomPollFixture.givenPollCreationInfo("pollTitle")
|
||||
val messageContent = RoomPollFixture.givenAMessagePollContent(pollCreationInfo)
|
||||
val timelineEvent = RoomPollFixture.givenATimelineEvent(
|
||||
eventId,
|
||||
A_ROOM_ID,
|
||||
AN_EVENT_TIMESTAMP,
|
||||
messageContent
|
||||
)
|
||||
every {
|
||||
timelineEvent.annotations
|
||||
?.pollResponseSummary
|
||||
?.aggregatedContent
|
||||
?.myVote
|
||||
} returns AN_EXISTING_OPTION_ID
|
||||
|
||||
every {
|
||||
fakeActiveSessionHolder.fakeSession
|
||||
.getRoom(A_ROOM_ID)
|
||||
?.getTimelineEvent(eventId)
|
||||
} returns timelineEvent
|
||||
}
|
||||
}
|
|
@ -19,9 +19,8 @@ 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
|
||||
import im.vector.app.test.fixtures.PollFixture.A_POLL_OPTION_IDS
|
||||
import im.vector.app.test.fixtures.PollFixture.A_POLL_RESPONSE_DATA
|
||||
|
@ -31,7 +30,6 @@ import io.mockk.verify
|
|||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
|
||||
class PollItemViewStateFactoryTest {
|
||||
|
||||
|
@ -46,18 +44,18 @@ class PollItemViewStateFactoryTest {
|
|||
@Test
|
||||
fun `given a sending poll state then poll is not votable and option states are PollSending`() {
|
||||
// Given
|
||||
val sendingPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(sendState = SendState.SENDING)
|
||||
val optionViewStates = listOf(PollOptionViewState.PollSending(optionId = "", optionAnswer = ""))
|
||||
every { fakePollOptionViewStateFactory.createPollSendingOptions(A_POLL_CONTENT.getBestPollCreationInfo()) } returns optionViewStates
|
||||
|
||||
// When
|
||||
val pollViewState = pollItemViewStateFactory.create(
|
||||
pollContent = A_POLL_CONTENT,
|
||||
informationData = sendingPollInformationData,
|
||||
pollResponseData = null,
|
||||
isSent = false,
|
||||
)
|
||||
|
||||
// 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,
|
||||
|
@ -70,7 +68,6 @@ class PollItemViewStateFactoryTest {
|
|||
fun `given a sent poll state when poll is closed then poll is not votable and option states are Ended`() {
|
||||
// Given
|
||||
val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true)
|
||||
val closedPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = closedPollSummary)
|
||||
val optionViewStates = listOf(
|
||||
PollOptionViewState.PollEnded(
|
||||
optionId = "", optionAnswer = "", voteCount = 0, votePercentage = 0.0, isWinner = false
|
||||
|
@ -79,18 +76,19 @@ class PollItemViewStateFactoryTest {
|
|||
every {
|
||||
fakePollOptionViewStateFactory.createPollEndedOptions(
|
||||
A_POLL_CONTENT.getBestPollCreationInfo(),
|
||||
closedPollInformationData.pollResponseAggregatedSummary,
|
||||
closedPollSummary,
|
||||
)
|
||||
} returns optionViewStates
|
||||
|
||||
// When
|
||||
val pollViewState = pollItemViewStateFactory.create(
|
||||
pollContent = A_POLL_CONTENT,
|
||||
informationData = closedPollInformationData,
|
||||
pollResponseData = closedPollSummary,
|
||||
isSent = true,
|
||||
)
|
||||
|
||||
// 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,
|
||||
|
@ -99,7 +97,7 @@ class PollItemViewStateFactoryTest {
|
|||
verify {
|
||||
fakePollOptionViewStateFactory.createPollEndedOptions(
|
||||
A_POLL_CONTENT.getBestPollCreationInfo(),
|
||||
closedPollInformationData.pollResponseAggregatedSummary,
|
||||
closedPollSummary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -108,7 +106,6 @@ class PollItemViewStateFactoryTest {
|
|||
fun `given a sent poll state with some decryption error when poll is closed then warning message is displayed`() {
|
||||
// Given
|
||||
val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true, hasEncryptedRelatedEvents = true)
|
||||
val closedPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = closedPollSummary)
|
||||
val optionViewStates = listOf(
|
||||
PollOptionViewState.PollEnded(
|
||||
optionId = "", optionAnswer = "", voteCount = 0, votePercentage = 0.0, isWinner = false
|
||||
|
@ -117,14 +114,15 @@ class PollItemViewStateFactoryTest {
|
|||
every {
|
||||
fakePollOptionViewStateFactory.createPollEndedOptions(
|
||||
A_POLL_CONTENT.getBestPollCreationInfo(),
|
||||
closedPollInformationData.pollResponseAggregatedSummary,
|
||||
closedPollSummary,
|
||||
)
|
||||
} returns optionViewStates
|
||||
|
||||
// When
|
||||
val pollViewState = pollItemViewStateFactory.create(
|
||||
pollContent = A_POLL_CONTENT,
|
||||
informationData = closedPollInformationData,
|
||||
pollResponseData = closedPollSummary,
|
||||
isSent = true,
|
||||
)
|
||||
|
||||
// Then
|
||||
|
@ -134,6 +132,7 @@ class PollItemViewStateFactoryTest {
|
|||
@Test
|
||||
fun `given a sent poll when undisclosed poll type is selected then poll is votable and option states are PollUndisclosed`() {
|
||||
// Given
|
||||
val pollResponseData = A_POLL_RESPONSE_DATA
|
||||
val optionViewStates = listOf(
|
||||
PollOptionViewState.PollUndisclosed(
|
||||
optionId = "",
|
||||
|
@ -144,18 +143,19 @@ class PollItemViewStateFactoryTest {
|
|||
every {
|
||||
fakePollOptionViewStateFactory.createPollUndisclosedOptions(
|
||||
A_POLL_CONTENT.getBestPollCreationInfo(),
|
||||
A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary,
|
||||
pollResponseData,
|
||||
)
|
||||
} returns optionViewStates
|
||||
|
||||
// When
|
||||
val pollViewState = pollItemViewStateFactory.create(
|
||||
pollContent = A_POLL_CONTENT,
|
||||
informationData = A_MESSAGE_INFORMATION_DATA,
|
||||
pollResponseData = pollResponseData,
|
||||
isSent = true,
|
||||
)
|
||||
|
||||
// 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,
|
||||
|
@ -164,7 +164,7 @@ class PollItemViewStateFactoryTest {
|
|||
verify {
|
||||
fakePollOptionViewStateFactory.createPollUndisclosedOptions(
|
||||
A_POLL_CONTENT.getBestPollCreationInfo(),
|
||||
A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary,
|
||||
A_POLL_RESPONSE_DATA,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -180,7 +180,6 @@ class PollItemViewStateFactoryTest {
|
|||
kind = PollType.DISCLOSED_UNSTABLE
|
||||
),
|
||||
)
|
||||
val votedInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = votedPollData)
|
||||
val optionViewStates = listOf(
|
||||
PollOptionViewState.PollVoted(
|
||||
optionId = "",
|
||||
|
@ -193,18 +192,19 @@ class PollItemViewStateFactoryTest {
|
|||
every {
|
||||
fakePollOptionViewStateFactory.createPollVotedOptions(
|
||||
disclosedPollContent.getBestPollCreationInfo(),
|
||||
votedInformationData.pollResponseAggregatedSummary,
|
||||
votedPollData,
|
||||
)
|
||||
} returns optionViewStates
|
||||
|
||||
// When
|
||||
val pollViewState = pollItemViewStateFactory.create(
|
||||
pollContent = disclosedPollContent,
|
||||
informationData = votedInformationData,
|
||||
pollResponseData = votedPollData,
|
||||
isSent = true,
|
||||
)
|
||||
|
||||
// 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,
|
||||
|
@ -213,7 +213,7 @@ class PollItemViewStateFactoryTest {
|
|||
verify {
|
||||
fakePollOptionViewStateFactory.createPollVotedOptions(
|
||||
disclosedPollContent.getBestPollCreationInfo(),
|
||||
votedInformationData.pollResponseAggregatedSummary,
|
||||
votedPollData,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -232,7 +232,6 @@ class PollItemViewStateFactoryTest {
|
|||
kind = PollType.DISCLOSED_UNSTABLE
|
||||
),
|
||||
)
|
||||
val votedInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = votedPollData)
|
||||
val optionViewStates = listOf(
|
||||
PollOptionViewState.PollVoted(
|
||||
optionId = "",
|
||||
|
@ -245,14 +244,15 @@ class PollItemViewStateFactoryTest {
|
|||
every {
|
||||
fakePollOptionViewStateFactory.createPollVotedOptions(
|
||||
disclosedPollContent.getBestPollCreationInfo(),
|
||||
votedInformationData.pollResponseAggregatedSummary,
|
||||
votedPollData,
|
||||
)
|
||||
} returns optionViewStates
|
||||
|
||||
// When
|
||||
val pollViewState = pollItemViewStateFactory.create(
|
||||
pollContent = disclosedPollContent,
|
||||
informationData = votedInformationData,
|
||||
pollResponseData = votedPollData,
|
||||
isSent = true,
|
||||
)
|
||||
|
||||
// Then
|
||||
|
@ -262,6 +262,7 @@ class PollItemViewStateFactoryTest {
|
|||
@Test
|
||||
fun `given a sent poll when poll type is disclosed then poll is votable and option view states are PollReady`() {
|
||||
// Given
|
||||
val pollResponseData = A_POLL_RESPONSE_DATA
|
||||
val disclosedPollContent = A_POLL_CONTENT.copy(
|
||||
unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy(
|
||||
kind = PollType.DISCLOSED_UNSTABLE
|
||||
|
@ -282,11 +283,12 @@ class PollItemViewStateFactoryTest {
|
|||
// When
|
||||
val pollViewState = pollItemViewStateFactory.create(
|
||||
pollContent = disclosedPollContent,
|
||||
informationData = A_MESSAGE_INFORMATION_DATA,
|
||||
pollResponseData = pollResponseData,
|
||||
isSent = true,
|
||||
)
|
||||
|
||||
// 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,
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* 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.domain
|
||||
|
||||
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkAll
|
||||
import org.amshove.kluent.shouldBe
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.isPollEnd
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
|
||||
private const val A_ROOM_ID = "room-id"
|
||||
private const val A_START_POLL_EVENT_ID = "start-poll-id"
|
||||
private const val AN_END_POLL_EVENT_ID = "end-poll-id"
|
||||
|
||||
internal class GetEndedPollEventIdUseCaseTest {
|
||||
|
||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
||||
|
||||
private val getEndedPollEventIdUseCase = GetEndedPollEventIdUseCase(
|
||||
activeSessionHolder = fakeActiveSessionHolder.instance
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockkStatic("org.matrix.android.sdk.api.session.events.model.EventKt")
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given existing related end event when execute then result is id of the end event`() {
|
||||
// Given
|
||||
val timelineEvent = givenEvent(eventId = AN_END_POLL_EVENT_ID, isPollEnd = true)
|
||||
givenARelatedEvent(timelineEvent = timelineEvent)
|
||||
|
||||
// When
|
||||
val result = getEndedPollEventIdUseCase.execute(A_ROOM_ID, A_START_POLL_EVENT_ID)
|
||||
|
||||
// Then
|
||||
result shouldBe AN_END_POLL_EVENT_ID
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given existing related event but not end poll event when execute then result is null`() {
|
||||
// Given
|
||||
val timelineEvent = givenEvent(eventId = AN_END_POLL_EVENT_ID, isPollEnd = false)
|
||||
givenARelatedEvent(timelineEvent = timelineEvent)
|
||||
|
||||
// When
|
||||
val result = getEndedPollEventIdUseCase.execute(A_ROOM_ID, A_START_POLL_EVENT_ID)
|
||||
|
||||
// Then
|
||||
result shouldBe null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given no existing related event when execute then result is null`() {
|
||||
// Given
|
||||
givenARelatedEvent(timelineEvent = null)
|
||||
|
||||
// When
|
||||
val result = getEndedPollEventIdUseCase.execute(A_ROOM_ID, A_START_POLL_EVENT_ID)
|
||||
|
||||
// Then
|
||||
result shouldBe null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given error occurred when execute then result is null`() {
|
||||
// Given
|
||||
givenErrorDuringRequest(error = Exception())
|
||||
|
||||
// When
|
||||
val result = getEndedPollEventIdUseCase.execute(A_ROOM_ID, A_START_POLL_EVENT_ID)
|
||||
|
||||
// Then
|
||||
result shouldBe null
|
||||
}
|
||||
|
||||
private fun givenEvent(eventId: String, isPollEnd: Boolean): TimelineEvent {
|
||||
val timelineEvent = mockk<TimelineEvent>()
|
||||
val event = mockk<Event>()
|
||||
every { timelineEvent.root } returns event
|
||||
every { timelineEvent.eventId } returns eventId
|
||||
every { event.isPollEnd() } returns isPollEnd
|
||||
return timelineEvent
|
||||
}
|
||||
|
||||
private fun givenARelatedEvent(timelineEvent: TimelineEvent?) {
|
||||
val result: List<TimelineEvent> = timelineEvent?.let { listOf(it) } ?: emptyList()
|
||||
every {
|
||||
fakeActiveSessionHolder.instance
|
||||
.getActiveSession()
|
||||
.roomService()
|
||||
.getRoom(A_ROOM_ID)
|
||||
?.timelineService()
|
||||
?.getTimelineEventsRelatedTo(RelationType.REFERENCE, A_START_POLL_EVENT_ID)
|
||||
} returns result
|
||||
}
|
||||
|
||||
private fun givenErrorDuringRequest(error: Exception) {
|
||||
every {
|
||||
fakeActiveSessionHolder.instance
|
||||
.getActiveSession()
|
||||
.roomService()
|
||||
.getRoom(A_ROOM_ID)
|
||||
?.timelineService()
|
||||
?.getTimelineEventsRelatedTo(RelationType.REFERENCE, A_START_POLL_EVENT_ID)
|
||||
} throws error
|
||||
}
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* 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.features.home.room.detail.timeline.factory.PollItemViewStateFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.PollResponseDataFactory
|
||||
import im.vector.app.features.poll.PollItemViewState
|
||||
import im.vector.app.features.roomprofile.polls.detail.domain.GetEndedPollEventIdUseCase
|
||||
import im.vector.app.test.fixtures.RoomPollFixture
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkAll
|
||||
import org.amshove.kluent.shouldBe
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
|
||||
private const val AN_EVENT_ID = "event-id"
|
||||
private const val A_ROOM_ID = "room-id"
|
||||
private const val AN_EVENT_TIMESTAMP = 123L
|
||||
|
||||
internal class RoomPollDetailMapperTest {
|
||||
|
||||
private val fakePollResponseDataFactory = mockk<PollResponseDataFactory>()
|
||||
private val fakePollItemViewStateFactory = mockk<PollItemViewStateFactory>()
|
||||
private val fakeGetEndedPollEventIdUseCase = mockk<GetEndedPollEventIdUseCase>()
|
||||
|
||||
private val roomPollDetailMapper = RoomPollDetailMapper(
|
||||
pollResponseDataFactory = fakePollResponseDataFactory,
|
||||
pollItemViewStateFactory = fakePollItemViewStateFactory,
|
||||
getEndedPollEventIdUseCase = fakeGetEndedPollEventIdUseCase,
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockkStatic("im.vector.app.core.extensions.TimelineEventKt")
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a not ended poll event when mapping to model then result contains correct poll details`() {
|
||||
// Given
|
||||
val aPollItemViewState = givenAPollItemViewState()
|
||||
val aPollEvent = givenAPollTimelineEvent(
|
||||
eventId = AN_EVENT_ID,
|
||||
creationTimestamp = AN_EVENT_TIMESTAMP,
|
||||
isClosed = false,
|
||||
pollItemViewState = aPollItemViewState,
|
||||
)
|
||||
val expectedResult = RoomPollDetail(
|
||||
creationTimestamp = AN_EVENT_TIMESTAMP,
|
||||
isEnded = false,
|
||||
endedPollEventId = null,
|
||||
pollItemViewState = aPollItemViewState,
|
||||
)
|
||||
|
||||
// When
|
||||
val result = roomPollDetailMapper.map(aPollEvent)
|
||||
|
||||
// Then
|
||||
result shouldBeEqualTo expectedResult
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given an ended poll event when mapping to model then result contains correct poll details`() {
|
||||
// Given
|
||||
val aPollItemViewState = givenAPollItemViewState()
|
||||
val aPollEvent = givenAPollTimelineEvent(
|
||||
eventId = AN_EVENT_ID,
|
||||
creationTimestamp = AN_EVENT_TIMESTAMP,
|
||||
isClosed = true,
|
||||
pollItemViewState = aPollItemViewState,
|
||||
)
|
||||
val endedPollEventId = givenEndedPollEventId()
|
||||
val expectedResult = RoomPollDetail(
|
||||
creationTimestamp = AN_EVENT_TIMESTAMP,
|
||||
isEnded = true,
|
||||
endedPollEventId = endedPollEventId,
|
||||
pollItemViewState = aPollItemViewState,
|
||||
)
|
||||
|
||||
// When
|
||||
val result = roomPollDetailMapper.map(aPollEvent)
|
||||
|
||||
// Then
|
||||
result shouldBeEqualTo expectedResult
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given missing data in event when mapping to model then result is null`() {
|
||||
// Given
|
||||
val aPollItemViewState = givenAPollItemViewState()
|
||||
val noIdPollEvent = givenAPollTimelineEvent(
|
||||
eventId = "",
|
||||
creationTimestamp = AN_EVENT_TIMESTAMP,
|
||||
isClosed = false,
|
||||
pollItemViewState = aPollItemViewState,
|
||||
)
|
||||
val noTimestampPollEvent = givenAPollTimelineEvent(
|
||||
eventId = AN_EVENT_ID,
|
||||
creationTimestamp = 0,
|
||||
isClosed = false,
|
||||
pollItemViewState = aPollItemViewState,
|
||||
)
|
||||
val notAPollEvent = RoomPollFixture.givenATimelineEvent(
|
||||
eventId = AN_EVENT_ID,
|
||||
roomId = "room-id",
|
||||
creationTimestamp = 0,
|
||||
content = mockk<MessageTextContent>(),
|
||||
)
|
||||
|
||||
// When
|
||||
val result1 = roomPollDetailMapper.map(noIdPollEvent)
|
||||
val result2 = roomPollDetailMapper.map(noTimestampPollEvent)
|
||||
val result3 = roomPollDetailMapper.map(notAPollEvent)
|
||||
|
||||
// Then
|
||||
result1 shouldBe null
|
||||
result2 shouldBe null
|
||||
result3 shouldBe null
|
||||
}
|
||||
|
||||
private fun givenAPollItemViewState(): PollItemViewState {
|
||||
return PollItemViewState(
|
||||
question = "",
|
||||
votesStatus = "",
|
||||
canVote = true,
|
||||
optionViewStates = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun givenAPollTimelineEvent(
|
||||
eventId: String,
|
||||
creationTimestamp: Long,
|
||||
isClosed: Boolean,
|
||||
pollItemViewState: PollItemViewState,
|
||||
): TimelineEvent {
|
||||
val pollCreationInfo = RoomPollFixture.givenPollCreationInfo("pollTitle")
|
||||
val messageContent = RoomPollFixture.givenAMessagePollContent(pollCreationInfo)
|
||||
val timelineEvent = RoomPollFixture.givenATimelineEvent(eventId, A_ROOM_ID, creationTimestamp, messageContent)
|
||||
val pollResponseData = RoomPollFixture.givenAPollResponseData(isClosed, totalVotes = 1)
|
||||
every { fakePollResponseDataFactory.create(timelineEvent) } returns pollResponseData
|
||||
every {
|
||||
fakePollItemViewStateFactory.create(
|
||||
pollContent = messageContent,
|
||||
pollResponseData = pollResponseData,
|
||||
isSent = true
|
||||
)
|
||||
} returns pollItemViewState
|
||||
|
||||
return timelineEvent
|
||||
}
|
||||
|
||||
private fun givenEndedPollEventId(): String {
|
||||
val eventId = "ended-poll-event-id"
|
||||
every {
|
||||
fakeGetEndedPollEventIdUseCase.execute(
|
||||
startPollEventId = AN_EVENT_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
)
|
||||
} returns eventId
|
||||
return eventId
|
||||
}
|
||||
}
|
|
@ -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 android.content.Context
|
||||
import im.vector.app.test.fakes.FakeNavigator
|
||||
import io.mockk.mockk
|
||||
import org.junit.Test
|
||||
|
||||
internal class RoomPollDetailNavigatorTest {
|
||||
|
||||
private val fakeNavigator = FakeNavigator()
|
||||
|
||||
private val roomPollDetailNavigator = RoomPollDetailNavigator(
|
||||
navigator = fakeNavigator.instance,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `given main navigator when goToTimelineEvent then correct method main navigator is called`() {
|
||||
// Given
|
||||
val aContext = mockk<Context>()
|
||||
val aRoomId = "roomId"
|
||||
val anEventId = "eventId"
|
||||
fakeNavigator.givenOpenRoomSuccess(
|
||||
context = aContext,
|
||||
roomId = aRoomId,
|
||||
eventId = anEventId,
|
||||
buildTask = true,
|
||||
isInviteAlreadyAccepted = false,
|
||||
trigger = null,
|
||||
)
|
||||
|
||||
// When
|
||||
roomPollDetailNavigator.goToTimelineEvent(aContext, aRoomId, anEventId)
|
||||
|
||||
// Then
|
||||
fakeNavigator.verifyOpenRoom(
|
||||
context = aContext,
|
||||
roomId = aRoomId,
|
||||
eventId = anEventId,
|
||||
buildTask = true,
|
||||
isInviteAlreadyAccepted = false,
|
||||
trigger = null,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* 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 com.airbnb.mvrx.test.MavericksTestRule
|
||||
import im.vector.app.core.event.GetTimelineEventUseCase
|
||||
import im.vector.app.features.home.room.detail.poll.VoteToPollUseCase
|
||||
import im.vector.app.test.test
|
||||
import im.vector.app.test.testDispatcher
|
||||
import io.mockk.every
|
||||
import io.mockk.justRun
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
|
||||
private const val A_POLL_ID = "poll-id"
|
||||
private const val A_ROOM_ID = "room-id"
|
||||
|
||||
internal class RoomPollDetailViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher)
|
||||
|
||||
private val initialState = RoomPollDetailViewState(pollId = A_POLL_ID, roomId = A_ROOM_ID)
|
||||
private val fakeGetTimelineEventUseCase = mockk<GetTimelineEventUseCase>()
|
||||
private val fakeRoomPollDetailMapper = mockk<RoomPollDetailMapper>()
|
||||
private val fakeVoteToPollUseCase = mockk<VoteToPollUseCase>()
|
||||
|
||||
private fun createViewModel(): RoomPollDetailViewModel {
|
||||
return RoomPollDetailViewModel(
|
||||
initialState = initialState,
|
||||
getTimelineEventUseCase = fakeGetTimelineEventUseCase,
|
||||
roomPollDetailMapper = fakeRoomPollDetailMapper,
|
||||
voteToPollUseCase = fakeVoteToPollUseCase,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given viewModel when created then poll detail is observed and viewState is updated`() {
|
||||
// Given
|
||||
val aPollEvent = givenAPollEvent()
|
||||
val pollDetail = givenAPollDetail()
|
||||
every { fakeGetTimelineEventUseCase.execute(A_ROOM_ID, A_POLL_ID) } returns flowOf(aPollEvent)
|
||||
every { fakeRoomPollDetailMapper.map(aPollEvent) } returns pollDetail
|
||||
val expectedViewState = initialState.copy(pollDetail = pollDetail)
|
||||
|
||||
// When
|
||||
val viewModel = createViewModel()
|
||||
val viewModelTest = viewModel.test()
|
||||
|
||||
// Then
|
||||
viewModelTest
|
||||
.assertLatestState(expectedViewState)
|
||||
.finish()
|
||||
verify {
|
||||
fakeGetTimelineEventUseCase.execute(A_ROOM_ID, A_POLL_ID)
|
||||
fakeRoomPollDetailMapper.map(aPollEvent)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given viewModel when handle vote action then correct use case is called`() {
|
||||
// Given
|
||||
val aPollEvent = givenAPollEvent()
|
||||
val pollDetail = givenAPollDetail()
|
||||
every { fakeGetTimelineEventUseCase.execute(A_ROOM_ID, A_POLL_ID) } returns flowOf(aPollEvent)
|
||||
every { fakeRoomPollDetailMapper.map(aPollEvent) } returns pollDetail
|
||||
val viewModel = createViewModel()
|
||||
val optionId = "option-id"
|
||||
justRun {
|
||||
fakeVoteToPollUseCase.execute(
|
||||
roomId = A_ROOM_ID,
|
||||
pollEventId = A_POLL_ID,
|
||||
optionId = optionId,
|
||||
)
|
||||
}
|
||||
val action = RoomPollDetailAction.Vote(
|
||||
pollEventId = A_POLL_ID,
|
||||
optionId = optionId,
|
||||
)
|
||||
|
||||
// When
|
||||
val viewModelTest = viewModel.test()
|
||||
viewModel.handle(action)
|
||||
|
||||
// Then
|
||||
viewModelTest.finish()
|
||||
verify {
|
||||
fakeVoteToPollUseCase.execute(
|
||||
roomId = A_ROOM_ID,
|
||||
pollEventId = A_POLL_ID,
|
||||
optionId = optionId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun givenAPollEvent(): TimelineEvent {
|
||||
return mockk()
|
||||
}
|
||||
|
||||
private fun givenAPollDetail(): RoomPollDetail {
|
||||
return RoomPollDetail(
|
||||
creationTimestamp = 123L,
|
||||
isEnded = false,
|
||||
endedPollEventId = null,
|
||||
pollItemViewState = mockk(),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -16,11 +16,10 @@
|
|||
|
||||
package im.vector.app.features.roomprofile.polls.list.ui
|
||||
|
||||
import im.vector.app.core.extensions.getVectorLastMessageContent
|
||||
import im.vector.app.features.home.room.detail.timeline.factory.PollOptionViewStateFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.PollResponseDataFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollResponseData
|
||||
import im.vector.app.test.fixtures.RoomPollFixture
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
|
@ -30,11 +29,7 @@ import org.amshove.kluent.shouldBeEqualTo
|
|||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollQuestion
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
|
||||
private const val AN_EVENT_ID = "event-id"
|
||||
|
@ -84,10 +79,12 @@ internal class PollSummaryMapperTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `given an ended poll event when mapping to model then result is ended poll`() {
|
||||
fun `given an ended poll event when mapping to model then result is ended poll with only winner options`() {
|
||||
// Given
|
||||
val totalVotes = 10
|
||||
val winnerOptions = listOf<PollOptionViewState.PollEnded>()
|
||||
val option1 = givenAPollEndedOption(isWinner = false)
|
||||
val option2 = givenAPollEndedOption(isWinner = true)
|
||||
val winnerOptions = listOf(option1, option2)
|
||||
val endedPollEvent = givenAPollTimelineEvent(
|
||||
eventId = AN_EVENT_ID,
|
||||
creationTimestamp = AN_EVENT_TIMESTAMP,
|
||||
|
@ -101,7 +98,7 @@ internal class PollSummaryMapperTest {
|
|||
creationTimestamp = AN_EVENT_TIMESTAMP,
|
||||
title = A_POLL_TITLE,
|
||||
totalVotes = totalVotes,
|
||||
winnerOptions = winnerOptions,
|
||||
winnerOptions = listOf(option2),
|
||||
)
|
||||
|
||||
// When
|
||||
|
@ -126,10 +123,11 @@ internal class PollSummaryMapperTest {
|
|||
pollTitle = A_POLL_TITLE,
|
||||
isClosed = false,
|
||||
)
|
||||
val notAPollEvent = givenATimelineEvent(
|
||||
val notAPollEvent = RoomPollFixture.givenATimelineEvent(
|
||||
eventId = AN_EVENT_ID,
|
||||
roomId = "room-id",
|
||||
creationTimestamp = 0,
|
||||
content = mockk<MessageTextContent>()
|
||||
content = mockk<MessageTextContent>(),
|
||||
)
|
||||
|
||||
// When
|
||||
|
@ -143,18 +141,6 @@ internal class PollSummaryMapperTest {
|
|||
result3 shouldBe null
|
||||
}
|
||||
|
||||
private fun givenATimelineEvent(
|
||||
eventId: String,
|
||||
creationTimestamp: Long,
|
||||
content: MessageContent,
|
||||
): TimelineEvent {
|
||||
val timelineEvent = mockk<TimelineEvent>()
|
||||
every { timelineEvent.root.eventId } returns eventId
|
||||
every { timelineEvent.root.originServerTs } returns creationTimestamp
|
||||
every { timelineEvent.getVectorLastMessageContent() } returns content
|
||||
return timelineEvent
|
||||
}
|
||||
|
||||
private fun givenAPollTimelineEvent(
|
||||
eventId: String,
|
||||
creationTimestamp: Long,
|
||||
|
@ -163,10 +149,10 @@ internal class PollSummaryMapperTest {
|
|||
totalVotes: Int = 0,
|
||||
winnerOptions: List<PollOptionViewState.PollEnded> = emptyList(),
|
||||
): TimelineEvent {
|
||||
val pollCreationInfo = givenPollCreationInfo(pollTitle)
|
||||
val messageContent = givenAMessagePollContent(pollCreationInfo)
|
||||
val timelineEvent = givenATimelineEvent(eventId, creationTimestamp, messageContent)
|
||||
val pollResponseData = givenAPollResponseData(isClosed, totalVotes)
|
||||
val pollCreationInfo = RoomPollFixture.givenPollCreationInfo(pollTitle)
|
||||
val messageContent = RoomPollFixture.givenAMessagePollContent(pollCreationInfo)
|
||||
val timelineEvent = RoomPollFixture.givenATimelineEvent(eventId, "room-id", creationTimestamp, messageContent)
|
||||
val pollResponseData = RoomPollFixture.givenAPollResponseData(isClosed, totalVotes)
|
||||
every { fakePollResponseDataFactory.create(timelineEvent) } returns pollResponseData
|
||||
every {
|
||||
fakePollOptionViewStateFactory.createPollEndedOptions(
|
||||
|
@ -178,24 +164,9 @@ internal class PollSummaryMapperTest {
|
|||
return timelineEvent
|
||||
}
|
||||
|
||||
private fun givenAMessagePollContent(pollCreationInfo: PollCreationInfo): MessagePollContent {
|
||||
return MessagePollContent(
|
||||
unstablePollCreationInfo = pollCreationInfo,
|
||||
)
|
||||
}
|
||||
|
||||
private fun givenPollCreationInfo(pollTitle: String): PollCreationInfo {
|
||||
return PollCreationInfo(
|
||||
question = PollQuestion(unstableQuestion = pollTitle),
|
||||
)
|
||||
}
|
||||
|
||||
private fun givenAPollResponseData(isClosed: Boolean, totalVotes: Int): PollResponseData {
|
||||
return PollResponseData(
|
||||
myVote = "",
|
||||
votes = emptyMap(),
|
||||
isClosed = isClosed,
|
||||
totalVotes = totalVotes,
|
||||
)
|
||||
private fun givenAPollEndedOption(isWinner: Boolean): PollOptionViewState.PollEnded {
|
||||
return mockk<PollOptionViewState.PollEnded>().also {
|
||||
every { it.isWinner } returns isWinner
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 im.vector.app.features.roomprofile.polls.list.ui
|
||||
|
||||
import android.content.Intent
|
||||
import im.vector.app.features.roomprofile.polls.detail.ui.RoomPollDetailActivity
|
||||
import im.vector.app.test.fakes.FakeContext
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.unmockkAll
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
internal class RoomPollsListNavigatorTest {
|
||||
|
||||
private val fakeContext = FakeContext()
|
||||
private val roomPollsListNavigator = RoomPollsListNavigator()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockkObject(RoomPollDetailActivity.Companion)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given info about poll when goToPollDetails then it starts the correct activity`() {
|
||||
// Given
|
||||
val aPollId = "pollId"
|
||||
val aRoomId = "roomId"
|
||||
val isEnded = true
|
||||
val intent = givenIntentForPollDetails(aPollId, aRoomId, isEnded)
|
||||
fakeContext.givenStartActivity(intent)
|
||||
|
||||
// When
|
||||
roomPollsListNavigator.goToPollDetails(fakeContext.instance, aPollId, aRoomId, isEnded)
|
||||
|
||||
// Then
|
||||
fakeContext.verifyStartActivity(intent)
|
||||
}
|
||||
|
||||
private fun givenIntentForPollDetails(pollId: String, roomId: String, isEnded: Boolean): Intent {
|
||||
val intent = mockk<Intent>()
|
||||
every { RoomPollDetailActivity.newIntent(fakeContext.instance, pollId, roomId, isEnded) } returns intent
|
||||
return intent
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.test.fakes
|
||||
|
||||
import android.content.Context
|
||||
import im.vector.app.features.analytics.plan.ViewRoom
|
||||
import im.vector.app.features.navigation.Navigator
|
||||
import io.mockk.justRun
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
|
||||
class FakeNavigator {
|
||||
|
||||
val instance: Navigator = mockk()
|
||||
|
||||
fun givenOpenRoomSuccess(
|
||||
context: Context,
|
||||
roomId: String,
|
||||
eventId: String?,
|
||||
buildTask: Boolean,
|
||||
isInviteAlreadyAccepted: Boolean,
|
||||
trigger: ViewRoom.Trigger?,
|
||||
) {
|
||||
justRun { instance.openRoom(context, roomId, eventId, buildTask, isInviteAlreadyAccepted, trigger) }
|
||||
}
|
||||
|
||||
fun verifyOpenRoom(
|
||||
context: Context,
|
||||
roomId: String,
|
||||
eventId: String?,
|
||||
buildTask: Boolean,
|
||||
isInviteAlreadyAccepted: Boolean,
|
||||
trigger: ViewRoom.Trigger?,
|
||||
) {
|
||||
verify { instance.openRoom(context, roomId, eventId, buildTask, isInviteAlreadyAccepted, trigger) }
|
||||
}
|
||||
}
|
|
@ -16,14 +16,26 @@
|
|||
|
||||
package im.vector.app.test.fakes
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
|
||||
class FakeTimelineService : TimelineService by mockk() {
|
||||
|
||||
fun givenTimelineEventReturns(eventId: String, event: TimelineEvent?) {
|
||||
every { getTimelineEvent(eventId) } returns event
|
||||
}
|
||||
|
||||
fun givenTimelineEventLiveReturns(
|
||||
eventId: String,
|
||||
event: TimelineEvent?
|
||||
): LiveData<Optional<TimelineEvent>> {
|
||||
return MutableLiveData(Optional(event)).also {
|
||||
every { getTimelineEventLive(eventId) } returns it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
65
vector/src/test/java/im/vector/app/test/fixtures/RoomPollFixture.kt
vendored
Normal file
65
vector/src/test/java/im/vector/app/test/fixtures/RoomPollFixture.kt
vendored
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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.test.fixtures
|
||||
|
||||
import im.vector.app.core.extensions.getVectorLastMessageContent
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollResponseData
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollQuestion
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
|
||||
object RoomPollFixture {
|
||||
|
||||
fun givenATimelineEvent(
|
||||
eventId: String,
|
||||
roomId: String,
|
||||
creationTimestamp: Long,
|
||||
content: MessageContent,
|
||||
): TimelineEvent {
|
||||
val timelineEvent = mockk<TimelineEvent>()
|
||||
every { timelineEvent.root.eventId } returns eventId
|
||||
every { timelineEvent.roomId } returns roomId
|
||||
every { timelineEvent.root.originServerTs } returns creationTimestamp
|
||||
every { timelineEvent.getVectorLastMessageContent() } returns content
|
||||
return timelineEvent
|
||||
}
|
||||
|
||||
fun givenAMessagePollContent(pollCreationInfo: PollCreationInfo): MessagePollContent {
|
||||
return MessagePollContent(
|
||||
unstablePollCreationInfo = pollCreationInfo,
|
||||
)
|
||||
}
|
||||
|
||||
fun givenPollCreationInfo(pollTitle: String): PollCreationInfo {
|
||||
return PollCreationInfo(
|
||||
question = PollQuestion(unstableQuestion = pollTitle),
|
||||
)
|
||||
}
|
||||
|
||||
fun givenAPollResponseData(isClosed: Boolean, totalVotes: Int): PollResponseData {
|
||||
return PollResponseData(
|
||||
myVote = "",
|
||||
votes = emptyMap(),
|
||||
isClosed = isClosed,
|
||||
totalVotes = totalVotes,
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue