Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Poll Feature - Timeline #4624

Merged
merged 36 commits into from
Dec 13, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e0abd99
Merge branch 'feature/ons/poll' into feature/ons/poll_timeline
Nov 9, 2021
94ae3c6
Merge branch 'feature/ons/poll' into feature/ons/poll_timeline
Nov 11, 2021
eb15197
Merge branch 'develop' into feature/ons/poll_timeline
Nov 16, 2021
ebc131f
Implement new poll content.
Dec 3, 2021
7c26930
Allow sending vote and ending poll.
Dec 3, 2021
a3b11b2
Allow removing poll event.
Dec 3, 2021
2a3a558
Aggregate votes and poll end event.
Dec 3, 2021
06485cf
Implement poll in timeline ui.
Dec 3, 2021
32e8a7e
Remove legacy poll ui.
Dec 3, 2021
c62028d
Implement poll actions bottom sheet.
Dec 3, 2021
0a7df44
Add confirmation dialog to end poll.
Dec 3, 2021
435dc9f
Fix room list preview.
Dec 3, 2021
23ad4e5
Remove legacy implementation classes.
Dec 3, 2021
1df6b33
Add labs flag for polls.
Dec 7, 2021
0d3444b
Fix poll option checkbox color.
Dec 7, 2021
75b544a
Support push notification for poll creation event.
Dec 7, 2021
71d7270
Add room list preview for poll response and end events.
Dec 7, 2021
566f633
Set max length for poll options.
Dec 7, 2021
953fade
Merge branch 'develop' into feature/ons/poll_timeline
Dec 7, 2021
0f11e49
Changelog added.
Dec 7, 2021
04a7590
Code review fixes.
Dec 9, 2021
b2e599e
Merge branch 'develop' into feature/ons/poll_timeline
Dec 9, 2021
d5f8931
Support to show hidden poll events as formatted.
Dec 9, 2021
be9e592
Do not allow to vote the same option twice.
Dec 9, 2021
9b2a3cf
Code review fixes.
Dec 10, 2021
c7ad50a
Code and design review fixes.
Dec 13, 2021
f6dcda6
Code review fixes.
Dec 13, 2021
f028f98
Merge branch 'develop' into feature/ons/poll_timeline
Dec 13, 2021
e2bbc3f
Code review fixes.
Dec 13, 2021
c1ea653
Reorder classes so that it follows the poll status logical order
bmarty Dec 13, 2021
eac06d5
Do some renaming
bmarty Dec 13, 2021
10b39cc
Do some renaming
bmarty Dec 13, 2021
da407ef
Avoid lateinit
bmarty Dec 13, 2021
f29e14f
Rename class
bmarty Dec 13, 2021
0981af3
Remove unused attribute
bmarty Dec 13, 2021
db81ec2
Recycle View a bit more
bmarty Dec 13, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1580,10 +1580,10 @@ class RoomDetailFragment @Inject constructor(
.show(
activity = requireActivity(),
askForReason = action.askForReason,
confirmationRes = R.string.delete_event_dialog_content,
confirmationRes = action.dialogDescriptionRes,
positiveRes = R.string.remove,
reasonHintRes = R.string.delete_event_dialog_reason_hint,
titleRes = R.string.delete_event_dialog_title
titleRes = action.dialogTitleRes
) { reason ->
roomDetailViewModel.handle(RoomDetailAction.RedactAction(action.eventId, reason))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ sealed class EventSharedAction(@StringRes val titleRes: Int,
data class Remove(val eventId: String) :
EventSharedAction(R.string.remove, R.drawable.ic_trash, true)

data class Redact(val eventId: String, val askForReason: Boolean) :
data class Redact(val eventId: String, val askForReason: Boolean, val dialogTitleRes: Int, val dialogDescriptionRes: Int) :
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to update the action text too?
Currently this is:
image

(I think, not tested on your branch though)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, design team agrees to use the existing item.

EventSharedAction(R.string.message_action_item_redact, R.drawable.ic_delete, true)

data class Cancel(val eventId: String, val force: Boolean) :
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,21 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
}

if (canRedact(timelineEvent, actionPermissions)) {
add(EventSharedAction.Redact(eventId, askForReason = informationData.senderId != session.myUserId))
if (timelineEvent.root.getClearType() == EventType.POLL_START) {
add(EventSharedAction.Redact(
eventId,
askForReason = informationData.senderId != session.myUserId,
dialogTitleRes = R.string.delete_poll_dialog_title,
dialogDescriptionRes = R.string.delete_poll_dialog_content
))
} else {
add(EventSharedAction.Redact(
eventId,
askForReason = informationData.senderId != session.myUserId,
dialogTitleRes = R.string.delete_event_dialog_title,
dialogDescriptionRes = R.string.delete_event_dialog_content
))
}
}

if (canCopy(msgType)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem_
import im.vector.app.features.home.room.detail.timeline.item.PollItem
import im.vector.app.features.home.room.detail.timeline.item.PollItem_
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem
import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem
Expand All @@ -70,6 +71,7 @@ import im.vector.app.features.media.VideoContentRenderer
import me.gujun.android.span.span
import org.commonmark.node.Document
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toModel
Expand Down Expand Up @@ -170,17 +172,59 @@ class MessageItemFactory @Inject constructor(
}
}

private fun buildPollContent(messageContent: MessagePollContent,
private fun buildPollContent(pollContent: MessagePollContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): PollItem? {
val optionViewStates = mutableListOf<PollOptionViewState>()

val pollResponseSummary = informationData.pollResponseAggregatedSummary
val isEnded = pollResponseSummary?.isClosed.orFalse()
val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse()
val winnerVoteCount = pollResponseSummary?.winnerVoteCount
val isPollSent = informationData.sendState.isSent()
val totalVotesText = (pollResponseSummary?.totalVotes ?: 0).let {
when {
isEnded -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, it, it)
didUserVoted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, it, it)
else -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, it, it)
}
}

pollContent.pollCreationInfo?.answers?.forEach { option ->
val voteSummary = pollResponseSummary?.votes?.get(option.id)
val isMyVote = pollResponseSummary?.myVote == option.id
val voteCount = voteSummary?.total ?: 0
val votePercentage = voteSummary?.percentage ?: 0.0
val optionName = option.answer ?: ""

optionViewStates.add(
if (!isPollSent) {
// Poll event is not send yet. Disable option.
PollOptionViewState.DisabledOptionWithInvisibleVotes(optionName)
} else if (isEnded) {
// Poll is ended. Disable option, show votes and mark the winner.
val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount
PollOptionViewState.DisabledOptionWithVisibleVotes(optionName, voteCount, votePercentage, isWinner)
} else if (didUserVoted) {
// User voted to the poll, but poll is not ended. Enable option, show votes and mark the user's selection.
PollOptionViewState.EnabledOptionWithVisibleVotes(optionName, voteCount, votePercentage, isMyVote)
} else {
// User didn't voted yet and poll is not ended yet. Enable options, hide votes.
PollOptionViewState.EnabledOptionWithInvisibleVotes(optionName)
}
)
}

return PollItem_()
.attributes(attributes)
.eventId(informationData.eventId)
.pollResponseSummary(informationData.pollResponseAggregatedSummary)
.pollSent(informationData.sendState.isSent())
.pollContent(messageContent)
.pollResponseSummary(pollResponseSummary)
.pollSent(isPollSent)
.pollContent(pollContent)
.totalVotesText(totalVotesText)
.optionViewStates(optionViewStates)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
.callback(callback)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ package im.vector.app.features.home.room.detail.timeline.item

import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.children
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent

@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
Expand All @@ -44,60 +44,44 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
@EpoxyAttribute
var pollSent: Boolean = false

@EpoxyAttribute
var totalVotesText: String? = null

@EpoxyAttribute
var optionViewStates: List<PollOptionViewState>? = null

override fun bind(holder: Holder) {
super.bind(holder)
val relatedEventId = eventId ?: return

renderSendState(holder.view, holder.questionTextView)

holder.questionTextView.text = pollContent?.pollCreationInfo?.question?.question
holder.totalVotesTextView.text = totalVotesText

holder.optionsContainer.removeAllViews()
val cachedViews = mutableMapOf<String, PollOptionItem>()
holder.optionsContainer.children.filterIsInstance<PollOptionItem>().forEach { existingPollItemView ->
cachedViews[existingPollItemView.getTag(STUB_ID)?.toString() ?: ""] = existingPollItemView
}

val isEnded = pollResponseSummary?.isClosed.orFalse()
val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse()
val totalVotes = pollResponseSummary?.totalVotes ?: 0
val winnerVoteCount = pollResponseSummary?.winnerVoteCount
holder.optionsContainer.removeAllViews()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe avoid to remove (and re-add later) all the View? The number of views may differ.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tested it and it is not working to remove items one by one before adding. Ends up with duplicated views.

Copy link
Member

@bmarty bmarty Dec 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Say you have 3 options to display. If there is:

  • 0 existing views: create 3 views and add it to the parent (this will be the most cases)
  • 4 existing views: just remove one, and do not add any
  • 3 existing views: do nothing
  • 2 existing views: create 1 view and add it to the parent

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point now. It will be an interesting experiment for me.


pollContent?.pollCreationInfo?.answers?.forEach { option ->
val voteSummary = pollResponseSummary?.votes?.get(option.id)
val isMyVote = pollResponseSummary?.myVote == option.id
val voteCount = voteSummary?.total ?: 0
val votePercentage = voteSummary?.percentage ?: 0.0
pollContent?.pollCreationInfo?.answers?.forEachIndexed { index, option ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not iterate on pollContent?.pollCreationInfo?.answers? anymore, but on optionViewStates now.

pollContent can be removed I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

val optionName = option.answer ?: ""

holder.optionsContainer.addView(
PollOptionItem(holder.view.context).apply {
val callback = object : PollOptionItem.Callback {
override fun onOptionClicked() {
callback?.onTimelineItemAction(RoomDetailAction.VoteToPoll(relatedEventId, option.id ?: ""))
}
}

if (!pollSent) {
// Poll event is not send yet. Disable option.
render(PollOptionViewState.DisabledOptionWithInvisibleVotes(optionName), callback)
} else if (isEnded) {
// Poll is ended. Disable option, show votes and mark the winner.
val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount
render(PollOptionViewState.DisabledOptionWithVisibleVotes(optionName, voteCount, votePercentage, isWinner), callback)
} else if (didUserVoted) {
// User voted to the poll, but poll is not ended. Enable option, show votes and mark the user's selection.
render(PollOptionViewState.EnabledOptionWithVisibleVotes(optionName, voteCount, votePercentage, isMyVote), callback)
} else {
// User didn't voted yet and poll is not ended yet. Enable options, hide votes.
render(PollOptionViewState.EnabledOptionWithInvisibleVotes(optionName), callback)
}
val tag = relatedEventId + option.id

val pollOptionItem: PollOptionItem = (cachedViews[tag] ?: PollOptionItem(holder.view.context))
.apply {
setTag(STUB_ID, tag)
render(
state = optionViewStates?.getOrNull(index) ?: PollOptionViewState.DisabledOptionWithInvisibleVotes(optionName)
)
}
)
}

holder.totalVotesTextView.apply {
text = when {
isEnded -> resources.getQuantityString(R.plurals.poll_total_vote_count_after_ended, totalVotes, totalVotes)
didUserVoted -> resources.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, totalVotes, totalVotes)
else -> resources.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, totalVotes, totalVotes)
pollOptionItem.setOnClickListener {
callback?.onTimelineItemAction(RoomDetailAction.VoteToPoll(relatedEventId, option.id ?: ""))
}

holder.optionsContainer.addView(pollOptionItem)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,7 @@ class PollOptionItem @JvmOverloads constructor(
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

item_poll_option.xml root view is also a ConstraintLayout, so the view hierarchy is too deep. You may remove a level.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't get it. It doesn't add a new level, just represents the current level, no?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that you are right.

image


interface Callback {
fun onOptionClicked()
}

private lateinit var views: ItemPollOptionBinding
private var callback: Callback? = null

init {
setupViews()
Expand All @@ -46,12 +41,9 @@ class PollOptionItem @JvmOverloads constructor(
private fun setupViews() {
inflate(context, R.layout.item_poll_option, this)
views = ItemPollOptionBinding.bind(this)

views.root.setOnClickListener { callback?.onOptionClicked() }
}

fun render(state: PollOptionViewState, callback: Callback) {
this.callback = callback
fun render(state: PollOptionViewState) {

views.optionNameTextView.text = state.name

Expand Down
2 changes: 1 addition & 1 deletion vector/src/main/res/layout/item_poll_option.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
style="@style/Widget.Vector.TextView.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:layout_marginEnd="8dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/optionVoteProgress"
app:layout_constraintEnd_toEndOf="parent"
Expand Down
2 changes: 2 additions & 0 deletions vector/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3678,4 +3678,6 @@
<string name="labs_enable_polls">Enable Polls</string>
<string name="poll_response_room_list_preview">Vote casted</string>
<string name="poll_end_room_list_preview">Poll ended</string>
<string name="delete_poll_dialog_title">Remove poll</string>
<string name="delete_poll_dialog_content">Are you sure you want to remove this poll? You won\'t be able to recover it once removed.</string>
</resources>