Reactions: start handling show more, show less and add more actions in timeline.

This commit is contained in:
ganfra 2022-02-09 21:29:03 +01:00
parent c28a20dce3
commit 911ee97122
10 changed files with 166 additions and 32 deletions

View File

@ -61,6 +61,8 @@
<dimen name="chat_bubble_fixed_size">300dp</dimen>
<dimen name="chat_bubble_corner_radius">12dp</dimen>
<dimen name="chat_reaction_min_height">30dp</dimen>
<!-- Onboarding -->
<item name="ftue_auth_gutter_start_percent" format="float" type="dimen">0.05</item>
<item name="ftue_auth_gutter_end_percent" format="float" type="dimen">0.95</item>

View File

@ -1913,6 +1913,10 @@ class TimelineFragment @Inject constructor(
timelineViewModel.handle(RoomDetailAction.LoadMoreTimelineEvents(direction))
}
override fun onAddMoreReaction(event: TimelineEvent) {
openEmojiReactionPicker(event.eventId)
}
override fun onEventCellClicked(informationData: MessageInformationData, messageContent: Any?, view: View, isRootThreadEvent: Boolean) {
when (messageContent) {
is MessageVerificationRequestContent -> {
@ -2117,7 +2121,7 @@ class TimelineFragment @Inject constructor(
openRoomMemberProfile(action.userId)
}
is EventSharedAction.AddReaction -> {
emojiActivityResultLauncher.launch(EmojiReactionPickerActivity.intent(requireContext(), action.eventId))
openEmojiReactionPicker(action.eventId)
}
is EventSharedAction.ViewReactions -> {
ViewReactionsBottomSheet.newInstance(timelineArgs.roomId, action.messageInformationData)
@ -2239,6 +2243,10 @@ class TimelineFragment @Inject constructor(
}
}
private fun openEmojiReactionPicker(eventId: String) {
emojiActivityResultLauncher.launch(EmojiReactionPickerActivity.intent(requireContext(), eventId))
}
private fun askConfirmationToEndPoll(eventId: String) {
MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Vector_MaterialAlertDialog)
.setTitle(R.string.end_poll_confirmation_title)

View File

@ -43,6 +43,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadSt
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
import im.vector.app.features.home.room.detail.timeline.helper.ReactionsSummaryFactory
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroups
@ -86,7 +87,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
@TimelineEventControllerHandler
private val backgroundHandler: Handler,
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper,
private val readReceiptsItemFactory: ReadReceiptsItemFactory
private val readReceiptsItemFactory: ReadReceiptsItemFactory,
private val reactionListFactory: ReactionsSummaryFactory
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
/**
@ -138,6 +140,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
fun getPreviewUrlRetriever(): PreviewUrlRetriever
fun onVoiceControlButtonClicked(eventId: String, messageAudioContent: MessageAudioContent)
fun onAddMoreReaction(event: TimelineEvent)
}
interface ReactionPillCallback {
@ -283,6 +287,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
super.onAttachedToRecyclerView(recyclerView)
timeline?.addListener(this)
timelineMediaSizeProvider.recyclerView = recyclerView
reactionListFactory.onRequestBuild = { requestModelBuild() }
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
@ -290,6 +295,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
contentUploadStateTrackerBinder.clear()
contentDownloadStateTrackerBinder.clear()
timeline?.removeListener(this)
reactionListFactory.onRequestBuild = null
super.onDetachedFromRecyclerView(recyclerView)
}
@ -383,7 +389,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val event = currentSnapshot[position]
val nextEvent = currentSnapshot.nextOrNull(position)
// Should be build if not cached or if model should be refreshed
if (modelCache[position] == null || modelCache[position]?.isCacheable(partialState) == false) {
if (modelCache[position] == null || modelCache[position]?.isCacheable(partialState) == false || reactionListFactory.needsRebuild(event)) {
val prevEvent = currentSnapshot.prevOrNull(position)
val prevDisplayableEvent = currentSnapshot.subList(0, position).lastOrNull {
timelineEventVisibilityHelper.shouldShowEvent(

View File

@ -24,7 +24,6 @@ import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration
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.home.room.detail.timeline.item.PollVoteSummaryData
import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData
import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayoutFactory
@ -50,7 +49,8 @@ import javax.inject.Inject
*/
class MessageInformationDataFactory @Inject constructor(private val session: Session,
private val dateFormatter: VectorDateFormatter,
private val messageLayoutFactory: TimelineMessageLayoutFactory) {
private val messageLayoutFactory: TimelineMessageLayoutFactory,
private val reactionListFactory: ReactionsSummaryFactory) {
fun create(params: TimelineItemFactoryParams): MessageInformationData {
val event = params.event
@ -93,11 +93,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
avatarUrl = event.senderInfo.avatarUrl,
memberName = event.senderInfo.disambiguatedDisplayName,
messageLayout = messageLayout,
orderedReactionList = event.annotations?.reactionsSummary
// ?.filter { isSingleEmoji(it.key) }
?.map {
ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
},
reactionsSummary = reactionListFactory.create(event, params.callback),
pollResponseAggregatedSummary = event.annotations?.pollResponseSummary?.let {
PollResponseData(
myVote = it.aggregatedContent?.myVote,

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) 2021 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.timeline.helper
import dagger.hilt.android.scopes.ActivityScoped
import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData
import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryData
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject
@ActivityScoped
class ReactionsSummaryFactory @Inject constructor() {
var onRequestBuild: (() -> Unit)? = null
private val showAllReactionsByEvent = HashSet<String>()
private val eventsRequestingBuild = HashSet<String>()
fun needsRebuild(event: TimelineEvent): Boolean {
return eventsRequestingBuild.remove(event.eventId)
}
fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): ReactionsSummaryData {
val eventId = event.eventId
val showAllStates = showAllReactionsByEvent.contains(eventId)
val reactions = event.annotations?.reactionsSummary
?.map {
ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
}
return ReactionsSummaryData(
reactions = reactions,
showAll = showAllStates,
onShowMoreClicked = {
showAllReactionsByEvent.add(eventId)
onRequestBuild(eventId)
},
onShowLessClicked = {
showAllReactionsByEvent.remove(eventId)
onRequestBuild(eventId)
},
onAddMoreClicked = {
callback?.onAddMoreReaction(event)
}
)
}
private fun onRequestBuild(eventId: String) {
eventsRequestingBuild.add(eventId)
onRequestBuild?.invoke()
}
}

View File

@ -16,11 +16,15 @@
package im.vector.app.features.home.room.detail.timeline.item
import android.annotation.SuppressLint
import android.content.Context
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.IdRes
import androidx.core.content.ContextCompat.getDrawable
import androidx.core.view.isVisible
import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
@ -65,27 +69,10 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
return listOf(baseAttributes.informationData.eventId)
}
@SuppressLint("SetTextI18n")
override fun bind(holder: H) {
super.bind(holder)
val reactions = baseAttributes.informationData.orderedReactionList
if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) {
holder.reactionsContainer.isVisible = false
} else {
holder.reactionsContainer.isVisible = true
holder.reactionsContainer.removeAllViews()
reactions.take(8).forEach { reaction ->
val reactionButton = ReactionButton(holder.view.context)
reactionButton.reactedListener = reactionClickListener
reactionButton.setTag(R.id.reactionsContainer, reaction.key)
reactionButton.reactionString = reaction.key
reactionButton.reactionCount = reaction.count
reactionButton.setChecked(reaction.addedByMe)
reactionButton.isEnabled = reaction.synced
holder.reactionsContainer.addView(reactionButton)
}
holder.reactionsContainer.setOnLongClickListener(baseAttributes.itemLongClickListener)
}
renderReactions(holder, baseAttributes.informationData.reactionsSummary)
when (baseAttributes.informationData.e2eDecoration) {
E2EDecoration.NONE -> {
holder.e2EDecorationView.render(null)
@ -102,6 +89,59 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
(holder.view as? TimelineMessageLayoutRenderer)?.renderMessageLayout(baseAttributes.informationData.messageLayout)
}
private fun renderReactions(holder: H, reactionsSummary: ReactionsSummaryData) {
val reactions = reactionsSummary.reactions
if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) {
holder.reactionsContainer.isVisible = false
} else {
holder.reactionsContainer.isVisible = true
holder.reactionsContainer.removeAllViews()
val reactionsToShow = if (reactionsSummary.showAll) {
reactions
} else {
reactions.take(8)
}
reactionsToShow.forEach { reaction ->
val reactionButton = ReactionButton(holder.view.context)
reactionButton.reactedListener = reactionClickListener
reactionButton.setTag(R.id.reactionsContainer, reaction.key)
reactionButton.reactionString = reaction.key
reactionButton.reactionCount = reaction.count
reactionButton.setChecked(reaction.addedByMe)
reactionButton.isEnabled = reaction.synced
holder.reactionsContainer.addView(reactionButton)
}
if (reactions.count() > 8) {
val showReactionsTextView = createReactionTextView(holder.view.context)
if (reactionsSummary.showAll) {
showReactionsTextView.setText(R.string.message_reaction_show_less)
showReactionsTextView.onClick { reactionsSummary.onShowLessClicked() }
} else {
val moreCount = reactions.count() - 8
showReactionsTextView.text = holder.view.resources.getString(R.string.message_reaction_show_more, moreCount)
showReactionsTextView.onClick { reactionsSummary.onShowMoreClicked() }
}
holder.reactionsContainer.addView(showReactionsTextView)
}
val addMoreReactionsTextView = createReactionTextView(holder.view.context)
addMoreReactionsTextView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_add_reaction_small, 0, 0, 0)
addMoreReactionsTextView.onClick { reactionsSummary.onAddMoreClicked() }
holder.reactionsContainer.addView(addMoreReactionsTextView)
holder.reactionsContainer.setOnLongClickListener(baseAttributes.itemLongClickListener)
}
}
private fun createReactionTextView(context: Context): TextView {
return TextView(context).apply {
textSize = 10f
gravity = Gravity.CENTER
minimumHeight = resources.getDimensionPixelSize(R.dimen.chat_reaction_min_height)
background = getDrawable(context, R.drawable.reaction_rounded_rect_shape_off)
val padding = resources.getDimensionPixelSize(R.dimen.layout_horizontal_margin)
setPadding(padding, 0, padding, 0)
}
}
override fun unbind(holder: H) {
holder.reactionsContainer.setOnLongClickListener(null)
super.unbind(holder)

View File

@ -33,8 +33,7 @@ data class MessageInformationData(
val avatarUrl: String?,
val memberName: CharSequence? = null,
val messageLayout: TimelineMessageLayout,
/*List of reactions (emoji,count,isSelected)*/
val orderedReactionList: List<ReactionInfoData>? = null,
val reactionsSummary: ReactionsSummaryData,
val pollResponseAggregatedSummary: PollResponseData? = null,
val hasBeenEdited: Boolean = false,
val hasPendingEdits: Boolean = false,
@ -55,6 +54,16 @@ data class ReferencesInfoData(
val verificationStatus: VerificationState
) : Parcelable
@Parcelize
data class ReactionsSummaryData(
/*List of reactions (emoji,count,isSelected)*/
val reactions: List<ReactionInfoData>? = null,
val showAll: Boolean = false,
val onShowMoreClicked: () -> Unit,
val onShowLessClicked: () -> Unit,
val onAddMoreClicked: () -> Unit
) : Parcelable
@Parcelize
data class ReactionInfoData(
val key: String,

View File

@ -68,7 +68,7 @@ class ReactionButton @JvmOverloads constructor(context: Context,
init {
inflate(context, R.layout.reaction_button, this)
orientation = HORIZONTAL
minimumHeight = DimensionConverter(context.resources).dpToPx(30)
minimumHeight = resources.getDimensionPixelSize(R.dimen.chat_reaction_min_height)
gravity = Gravity.CENTER
layoutDirection = View.LAYOUT_DIRECTION_LOCALE
views = ReactionButtonBinding.bind(this)

View File

@ -0,0 +1,4 @@
<vector android:height="14dp" android:viewportHeight="16"
android:viewportWidth="16" android:width="14dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#737D8C" android:fillType="evenOdd" android:pathData="M13.3334,0.667C12.9652,0.667 12.6667,0.9655 12.6667,1.3337V2.667L11.3334,2.667C10.9652,2.667 10.6667,2.9655 10.6667,3.3337C10.6667,3.7018 10.9652,4.0003 11.3334,4.0003H12.6667V5.3337C12.6667,5.7018 12.9652,6.0003 13.3334,6.0003C13.7016,6.0003 14,5.7018 14,5.3337V4.0003H15.3334C15.7016,4.0003 16,3.7018 16,3.3337C16,2.9655 15.7016,2.667 15.3334,2.667L14,2.667V1.3337C14,0.9655 13.7016,0.667 13.3334,0.667ZM4.6667,6.3337C4.6667,5.7803 5.1134,5.3337 5.6667,5.3337C6.22,5.3337 6.6667,5.7803 6.6667,6.3337C6.6667,6.887 6.22,7.3337 5.6667,7.3337C5.1134,7.3337 4.6667,6.887 4.6667,6.3337ZM10.3334,7.3337C10.8867,7.3337 11.3334,6.887 11.3334,6.3337C11.3334,5.7803 10.8867,5.3337 10.3334,5.3337C9.78,5.3337 9.3334,5.7803 9.3334,6.3337C9.3334,6.887 9.78,7.3337 10.3334,7.3337ZM8,11.667C9.5534,11.667 10.8734,10.6937 11.4067,9.3337H4.5934C5.1267,10.6937 6.4467,11.667 8,11.667ZM2.6667,8.0003C2.6667,5.0548 5.0545,2.667 8,2.667C8.4073,2.667 8.803,2.7125 9.1828,2.7985C9.542,2.8797 9.8989,2.6545 9.9802,2.2954C10.0615,1.9363 9.8362,1.5793 9.4771,1.498C9.0014,1.3903 8.5069,1.3337 8,1.3337C4.3181,1.3337 1.3334,4.3184 1.3334,8.0003C1.3334,11.6822 4.3181,14.667 8,14.667C11.6819,14.667 14.6667,11.6822 14.6667,8.0003C14.6667,7.8589 14.6623,7.7184 14.6536,7.579C14.6306,7.2115 14.3141,6.9322 13.9467,6.9552C13.5792,6.9781 13.2999,7.2946 13.3228,7.6621C13.3298,7.7738 13.3334,7.8866 13.3334,8.0003C13.3334,10.9458 10.9456,13.3337 8,13.3337C5.0545,13.3337 2.6667,10.9458 2.6667,8.0003Z"/>
</vector>

View File

@ -3779,4 +3779,7 @@
<string name="tooltip_attachment_poll">Create poll</string>
<string name="tooltip_attachment_location">Share location</string>
<string name="message_reaction_show_less">Show less</string>
<string name="message_reaction_show_more">"%1$d more"</string>
</resources>