diff --git a/changelog.d/8056.feature b/changelog.d/8056.feature
new file mode 100644
index 0000000000..ff000c98ff
--- /dev/null
+++ b/changelog.d/8056.feature
@@ -0,0 +1,2 @@
+[Poll] History list: details screen of a poll
+[Poll] History list: enable the new settings entry in release mode
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 2058d13d1d..66c66bbcbe 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3211,6 +3211,7 @@
Displaying polls
Load more polls
Error fetching polls.
+ View poll in timeline
Share location
diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index 9c8186b2d4..922d4ca292 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -327,6 +327,7 @@
+
diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
index 911bbfa4a3..c2e2f9f695 100644
--- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
@@ -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<*, *>
}
diff --git a/vector/src/main/java/im/vector/app/core/event/GetTimelineEventUseCase.kt b/vector/src/main/java/im/vector/app/core/event/GetTimelineEventUseCase.kt
new file mode 100644
index 0000000000..4265aac53e
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/event/GetTimelineEventUseCase.kt
@@ -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 {
+ return activeSessionHolder.getActiveSession().getRoom(roomId)
+ ?.timelineService()
+ ?.getTimelineEventLive(eventId)
+ ?.asFlow()
+ ?.unwrap()
+ ?: emptyFlow()
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
index 72d9fc8a16..5d5aae66bb 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
@@ -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(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) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/poll/VoteToPollUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/poll/VoteToPollUseCase.kt
new file mode 100644
index 0000000000..62f8006988
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/poll/VoteToPollUseCase.kt
@@ -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") }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index 67983fc351..bbe8e92756 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -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)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt
index 3c1a1cfd85..b630a514e4 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt
@@ -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,
diff --git a/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt b/vector/src/main/java/im/vector/app/features/poll/PollItemViewState.kt
similarity index 96%
rename from vector/src/main/java/im/vector/app/features/poll/PollViewState.kt
rename to vector/src/main/java/im/vector/app/features/poll/PollItemViewState.kt
index ecbee7438a..e5b4f71f1d 100644
--- a/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/poll/PollItemViewState.kt
@@ -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,
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt
index 30bd6c7ed3..1fbfaba2bb 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt
@@ -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),
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
index 91f57d33e9..9436bafc03 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
@@ -64,7 +64,7 @@ import javax.inject.Inject
@Parcelize
data class RoomProfileArgs(
- val roomId: String
+ val roomId: String,
) : Parcelable
@AndroidEntryPoint
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/domain/GetEndedPollEventIdUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/domain/GetEndedPollEventIdUseCase.kt
new file mode 100644
index 0000000000..aa1ba1b274
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/domain/GetEndedPollEventIdUseCase.kt
@@ -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()
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetail.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetail.kt
new file mode 100644
index 0000000000..7857a30eeb
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetail.kt
@@ -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,
+)
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailAction.kt
new file mode 100644
index 0000000000..dbf8436399
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailAction.kt
@@ -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
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailActivity.kt
new file mode 100644
index 0000000000..cf29d5618a
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailActivity.kt
@@ -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() {
+
+ 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)
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailController.kt
new file mode 100644
index 0000000000..7a246f812b
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailController.kt
@@ -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() {
+
+ 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)
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailFragment.kt
new file mode 100644
index 0000000000..9c118bb897
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailFragment.kt
@@ -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(),
+ 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,
+ )
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailItem.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailItem.kt
new file mode 100644
index 0000000000..b3f905e661
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailItem.kt
@@ -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(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
+
+ @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(R.id.pollDetailDate)
+ val questionTextView by bind(R.id.questionTextView)
+ val optionsContainer by bind(R.id.optionsContainer)
+ val votesStatusTextView by bind(R.id.optionsVotesStatusTextView)
+ val endedPollTextView by bind(R.id.endedPollTextView)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailMapper.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailMapper.kt
new file mode 100644
index 0000000000..8f14118d43
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailMapper.kt
@@ -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
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailNavigator.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailNavigator.kt
new file mode 100644
index 0000000000..a19bb87d9e
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailNavigator.kt
@@ -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,
+ )
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailViewModel.kt
new file mode 100644
index 0000000000..487595d20b
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailViewModel.kt
@@ -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(initialState) {
+
+ @AssistedFactory
+ interface Factory : MavericksAssistedViewModelFactory {
+ override fun create(initialState: RoomPollDetailViewState): RoomPollDetailViewModel
+ }
+
+ companion object : MavericksViewModelFactory 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,
+ )
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailViewState.kt
new file mode 100644
index 0000000000..a2906dc88f
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailViewState.kt
@@ -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,
+ )
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollGoToTimelineItem.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollGoToTimelineItem.kt
new file mode 100644
index 0000000000..59a5539a4f
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollGoToTimelineItem.kt
@@ -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(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