Merge pull request #5204 from vector-im/feature/fga/reactions_ui_improvements

Feature/fga/reactions UI improvements
This commit is contained in:
ganfra 2022-02-11 15:17:44 +01:00 committed by GitHub
commit f1376eac82
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 209 additions and 51 deletions

1
changelog.d/5204.feature Normal file
View file

@ -0,0 +1 @@
Improve UI of reactions in timeline, including quick add reaction.

View file

@ -23,4 +23,15 @@
<item name="android:backgroundTint">?vctr_content_quinary</item> <item name="android:backgroundTint">?vctr_content_quinary</item>
</style> </style>
<style name="TimelineReactionView">
<item name="android:paddingStart">6dp</item>
<item name="android:paddingEnd">6dp</item>
<item name="android:paddingTop">1dp</item>
<item name="android:paddingBottom">1dp</item>
<item name="android:minHeight">28dp</item>
<item name="android:minWidth">40dp</item>
<item name="android:gravity">center</item>
</style>
</resources> </resources>

View file

@ -66,11 +66,11 @@ internal class UIEchoManager(private val listener: Listener) {
return existingState != sendState return existingState != sendState
} }
fun onLocalEchoCreated(timelineEvent: TimelineEvent): Boolean { fun onLocalEchoCreated(timelineEvent: TimelineEvent): Boolean {
when (timelineEvent.root.getClearType()) { when (timelineEvent.root.getClearType()) {
EventType.REDACTION -> { EventType.REDACTION -> {
} }
EventType.REACTION -> { EventType.REACTION -> {
val content: ReactionContent? = timelineEvent.root.content?.toModel<ReactionContent>() val content: ReactionContent? = timelineEvent.root.content?.toModel<ReactionContent>()
if (RelationType.ANNOTATION == content?.relatesTo?.type) { if (RelationType.ANNOTATION == content?.relatesTo?.type) {
val reaction = content.relatesTo.key val reaction = content.relatesTo.key
@ -104,8 +104,8 @@ internal class UIEchoManager(private val listener: Listener) {
val updateReactions = existingAnnotationSummary.reactionsSummary.toMutableList() val updateReactions = existingAnnotationSummary.reactionsSummary.toMutableList()
contents.forEach { uiEchoReaction -> contents.forEach { uiEchoReaction ->
val existing = updateReactions.firstOrNull { it.key == uiEchoReaction.reaction } val indexOfExistingReaction = updateReactions.indexOfFirst { it.key == uiEchoReaction.reaction }
if (existing == null) { if (indexOfExistingReaction == -1) {
// just add the new key // just add the new key
ReactionAggregatedSummary( ReactionAggregatedSummary(
key = uiEchoReaction.reaction, key = uiEchoReaction.reaction,
@ -117,6 +117,7 @@ internal class UIEchoManager(private val listener: Listener) {
).let { updateReactions.add(it) } ).let { updateReactions.add(it) }
} else { } else {
// update Existing Key // update Existing Key
val existing = updateReactions[indexOfExistingReaction]
if (!existing.localEchoEvents.contains(uiEchoReaction.localEchoId)) { if (!existing.localEchoEvents.contains(uiEchoReaction.localEchoId)) {
updateReactions.remove(existing) updateReactions.remove(existing)
// only update if echo is not yet there // only update if echo is not yet there
@ -128,7 +129,7 @@ internal class UIEchoManager(private val listener: Listener) {
sourceEvents = existing.sourceEvents, sourceEvents = existing.sourceEvents,
localEchoEvents = existing.localEchoEvents + uiEchoReaction.localEchoId localEchoEvents = existing.localEchoEvents + uiEchoReaction.localEchoId
).let { updateReactions.add(it) } ).let { updateReactions.add(indexOfExistingReaction, it) }
} }
} }
} }

View file

@ -18,6 +18,9 @@ package im.vector.app.core.extensions
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ImageSpan
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
@ -34,6 +37,16 @@ fun Context.singletonEntryPoint(): SingletonEntryPoint {
return EntryPoints.get(applicationContext, SingletonEntryPoint::class.java) return EntryPoints.get(applicationContext, SingletonEntryPoint::class.java)
} }
fun Context.getDrawableAsSpannable(@DrawableRes drawableRes: Int, alignment: Int = ImageSpan.ALIGN_BOTTOM): Spannable {
return SpannableString(" ").apply {
val span = ContextCompat.getDrawable(this@getDrawableAsSpannable, drawableRes)?.let {
it.setBounds(0, 0, it.intrinsicWidth, it.intrinsicHeight)
ImageSpan(it, alignment)
}
setSpan(span, 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
}
}
fun Context.getResTintedDrawable(@DrawableRes drawableRes: Int, @ColorRes tint: Int, @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1f): Drawable? { fun Context.getResTintedDrawable(@DrawableRes drawableRes: Int, @ColorRes tint: Int, @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1f): Drawable? {
return getTintedDrawable(drawableRes, ContextCompat.getColor(this, tint), alpha) return getTintedDrawable(drawableRes, ContextCompat.getColor(this, tint), alpha)
} }

View file

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

View file

@ -41,6 +41,7 @@ import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFact
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.ReactionsSummaryFactory
import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper 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.TimelineEventDiffUtilCallback
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
@ -86,7 +87,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
@TimelineEventControllerHandler @TimelineEventControllerHandler
private val backgroundHandler: Handler, private val backgroundHandler: Handler,
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper, private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper,
private val readReceiptsItemFactory: ReadReceiptsItemFactory private val readReceiptsItemFactory: ReadReceiptsItemFactory,
private val reactionListFactory: ReactionsSummaryFactory
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
/** /**
@ -138,6 +140,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
fun getPreviewUrlRetriever(): PreviewUrlRetriever fun getPreviewUrlRetriever(): PreviewUrlRetriever
fun onVoiceControlButtonClicked(eventId: String, messageAudioContent: MessageAudioContent) fun onVoiceControlButtonClicked(eventId: String, messageAudioContent: MessageAudioContent)
fun onAddMoreReaction(event: TimelineEvent)
} }
interface ReactionPillCallback { interface ReactionPillCallback {
@ -283,6 +287,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
super.onAttachedToRecyclerView(recyclerView) super.onAttachedToRecyclerView(recyclerView)
timeline?.addListener(this) timeline?.addListener(this)
timelineMediaSizeProvider.recyclerView = recyclerView timelineMediaSizeProvider.recyclerView = recyclerView
reactionListFactory.onRequestBuild = { requestModelBuild() }
} }
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
@ -290,6 +295,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
contentUploadStateTrackerBinder.clear() contentUploadStateTrackerBinder.clear()
contentDownloadStateTrackerBinder.clear() contentDownloadStateTrackerBinder.clear()
timeline?.removeListener(this) timeline?.removeListener(this)
reactionListFactory.onRequestBuild = null
super.onDetachedFromRecyclerView(recyclerView) super.onDetachedFromRecyclerView(recyclerView)
} }
@ -383,7 +389,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val event = currentSnapshot[position] val event = currentSnapshot[position]
val nextEvent = currentSnapshot.nextOrNull(position) val nextEvent = currentSnapshot.nextOrNull(position)
// Should be build if not cached or if model should be refreshed // 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 prevEvent = currentSnapshot.prevOrNull(position)
val prevDisplayableEvent = currentSnapshot.subList(0, position).lastOrNull { val prevDisplayableEvent = currentSnapshot.subList(0, position).lastOrNull {
timelineEventVisibilityHelper.shouldShowEvent( 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.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.PollResponseData 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.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.ReferencesInfoData
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayoutFactory 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, class MessageInformationDataFactory @Inject constructor(private val session: Session,
private val dateFormatter: VectorDateFormatter, private val dateFormatter: VectorDateFormatter,
private val messageLayoutFactory: TimelineMessageLayoutFactory) { private val messageLayoutFactory: TimelineMessageLayoutFactory,
private val reactionsSummaryFactory: ReactionsSummaryFactory) {
fun create(params: TimelineItemFactoryParams): MessageInformationData { fun create(params: TimelineItemFactoryParams): MessageInformationData {
val event = params.event val event = params.event
@ -93,11 +93,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
avatarUrl = event.senderInfo.avatarUrl, avatarUrl = event.senderInfo.avatarUrl,
memberName = event.senderInfo.disambiguatedDisplayName, memberName = event.senderInfo.disambiguatedDisplayName,
messageLayout = messageLayout, messageLayout = messageLayout,
orderedReactionList = event.annotations?.reactionsSummary reactionsSummary = reactionsSummaryFactory.create(event, params.callback),
// ?.filter { isSingleEmoji(it.key) }
?.map {
ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
},
pollResponseAggregatedSummary = event.annotations?.pollResponseSummary?.let { pollResponseAggregatedSummary = event.annotations?.pollResponseSummary?.let {
PollResponseData( PollResponseData(
myVote = it.aggregatedContent?.myVote, myVote = it.aggregatedContent?.myVote,

View file

@ -0,0 +1,65 @@
/*
* 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.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,24 +16,34 @@
package im.vector.app.features.home.room.detail.timeline.item package im.vector.app.features.home.room.detail.timeline.item
import android.annotation.SuppressLint
import android.graphics.Typeface
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.content.ContextCompat.getDrawable
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.TextViewCompat
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.getDrawableAsSpannable
import im.vector.app.core.ui.views.ShieldImageView import im.vector.app.core.ui.views.ShieldImageView
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer
import im.vector.app.features.reactions.widget.ReactionButton import im.vector.app.features.reactions.widget.ReactionButton
import im.vector.app.features.themes.ThemeUtils
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
private const val MAX_REACTIONS_TO_SHOW = 8
/** /**
* Base timeline item with reactions and read receipts. * Base timeline item with reactions and read receipts.
* Manages associated click listeners and send status. * Manages associated click listeners and send status.
@ -65,27 +75,10 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
return listOf(baseAttributes.informationData.eventId) return listOf(baseAttributes.informationData.eventId)
} }
@SuppressLint("SetTextI18n")
override fun bind(holder: H) { override fun bind(holder: H) {
super.bind(holder) super.bind(holder)
val reactions = baseAttributes.informationData.orderedReactionList renderReactions(holder, baseAttributes.informationData.reactionsSummary)
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)
}
when (baseAttributes.informationData.e2eDecoration) { when (baseAttributes.informationData.e2eDecoration) {
E2EDecoration.NONE -> { E2EDecoration.NONE -> {
holder.e2EDecorationView.render(null) holder.e2EDecorationView.render(null)
@ -102,6 +95,58 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
(holder.view as? TimelineMessageLayoutRenderer)?.renderMessageLayout(baseAttributes.informationData.messageLayout) (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(MAX_REACTIONS_TO_SHOW)
}
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() > MAX_REACTIONS_TO_SHOW) {
val showReactionsTextView = createReactionTextView(holder)
if (reactionsSummary.showAll) {
showReactionsTextView.setText(R.string.message_reaction_show_less)
showReactionsTextView.onClick { reactionsSummary.onShowLessClicked() }
} else {
val moreCount = reactions.count() - MAX_REACTIONS_TO_SHOW
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)
addMoreReactionsTextView.text = holder.view.context.getDrawableAsSpannable(R.drawable.ic_add_reaction_small)
addMoreReactionsTextView.onClick { reactionsSummary.onAddMoreClicked() }
holder.reactionsContainer.addView(addMoreReactionsTextView)
holder.reactionsContainer.setOnLongClickListener(baseAttributes.itemLongClickListener)
}
}
private fun createReactionTextView(holder: H): TextView {
return TextView(ContextThemeWrapper(holder.view.context, R.style.TimelineReactionView)).apply {
background = getDrawable(context, R.drawable.reaction_rounded_rect_shape_off)
TextViewCompat.setTextAppearance(this, R.style.TextAppearance_Vector_Micro)
setTypeface(typeface, Typeface.BOLD)
setTextColor(ThemeUtils.getColor(context, R.attr.vctr_content_secondary))
}
}
override fun unbind(holder: H) { override fun unbind(holder: H) {
holder.reactionsContainer.setOnLongClickListener(null) holder.reactionsContainer.setOnLongClickListener(null)
super.unbind(holder) super.unbind(holder)
@ -115,6 +160,9 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
} }
abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) { abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) {
val dimensionConverter by lazy {
DimensionConverter(view.resources)
}
val reactionsContainer by bind<ViewGroup>(R.id.reactionsContainer) val reactionsContainer by bind<ViewGroup>(R.id.reactionsContainer)
val e2EDecorationView by bind<ShieldImageView>(R.id.messageE2EDecoration) val e2EDecorationView by bind<ShieldImageView>(R.id.messageE2EDecoration)
} }

View file

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

View file

@ -18,7 +18,6 @@ package im.vector.app.features.reactions.widget
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.Gravity
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -26,7 +25,6 @@ import androidx.core.content.withStyledAttributes
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.EmojiSpanify import im.vector.app.EmojiSpanify
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.core.utils.TextUtils import im.vector.app.core.utils.TextUtils
import im.vector.app.databinding.ReactionButtonBinding import im.vector.app.databinding.ReactionButtonBinding
import javax.inject.Inject import javax.inject.Inject
@ -38,8 +36,9 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ReactionButton @JvmOverloads constructor(context: Context, class ReactionButton @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : defStyleAttr: Int = 0,
LinearLayout(context, attrs, defStyleAttr), View.OnClickListener, View.OnLongClickListener { defStyleRes: Int = R.style.TimelineReactionView) :
LinearLayout(context, attrs, defStyleAttr, defStyleRes), View.OnClickListener, View.OnLongClickListener {
@Inject lateinit var emojiSpanify: EmojiSpanify @Inject lateinit var emojiSpanify: EmojiSpanify
@ -68,8 +67,6 @@ class ReactionButton @JvmOverloads constructor(context: Context,
init { init {
inflate(context, R.layout.reaction_button, this) inflate(context, R.layout.reaction_button, this)
orientation = HORIZONTAL orientation = HORIZONTAL
minimumHeight = DimensionConverter(context.resources).dpToPx(30)
gravity = Gravity.CENTER
layoutDirection = View.LAYOUT_DIRECTION_LOCALE layoutDirection = View.LAYOUT_DIRECTION_LOCALE
views = ReactionButtonBinding.bind(this) views = ReactionButtonBinding.bind(this)
views.reactionCount.text = TextUtils.formatCountToShortDecimal(reactionCount) views.reactionCount.text = TextUtils.formatCountToShortDecimal(reactionCount)

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

@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<size <size
android:width="8dp" android:width="4dp"
android:height="8dp" /> android:height="4dp" />
<solid android:color="#00000000" /> <solid android:color="#00000000" />
</shape> </shape>

View file

@ -3,11 +3,10 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="26dp" android:layout_height="wrap_content"
android:background="@drawable/reaction_rounded_rect_shape" android:background="@drawable/reaction_rounded_rect_shape"
android:clipChildren="false" android:clipChildren="false"
android:gravity="center" android:gravity="center"
android:minWidth="44dp"
tools:parentTag="android.widget.LinearLayout"> tools:parentTag="android.widget.LinearLayout">
<!--<View--> <!--<View-->
@ -20,12 +19,10 @@
android:id="@+id/reactionText" android:id="@+id/reactionText"
style="@style/Widget.Vector.TextView.Caption" style="@style/Widget.Vector.TextView.Caption"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="20dp" android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:ellipsize="middle" android:ellipsize="middle"
android:gravity="center" android:gravity="center"
android:maxEms="10" android:maxEms="10"
android:minWidth="20dp"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/emoji_color" android:textColor="@color/emoji_color"
tools:text="* Party Parrot Again * 👀" /> tools:text="* Party Parrot Again * 👀" />
@ -36,7 +33,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="2dp" android:layout_marginStart="2dp"
android:layout_marginEnd="8dp"
android:gravity="center" android:gravity="center"
android:maxLines="1" android:maxLines="1"
android:textColor="?vctr_content_secondary" android:textColor="?vctr_content_secondary"

View file

@ -177,7 +177,7 @@
<com.google.android.flexbox.FlexboxLayout <com.google.android.flexbox.FlexboxLayout
android:id="@+id/reactionsContainer" android:id="@+id/reactionsContainer"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
app:dividerDrawable="@drawable/reaction_divider" app:dividerDrawable="@drawable/reaction_divider"

View file

@ -3779,4 +3779,7 @@
<string name="tooltip_attachment_poll">Create poll</string> <string name="tooltip_attachment_poll">Create poll</string>
<string name="tooltip_attachment_location">Share location</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> </resources>