mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 02:15:35 +03:00
Merge pull request #5204 from vector-im/feature/fga/reactions_ui_improvements
Feature/fga/reactions UI improvements
This commit is contained in:
commit
f1376eac82
16 changed files with 209 additions and 51 deletions
1
changelog.d/5204.feature
Normal file
1
changelog.d/5204.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Improve UI of reactions in timeline, including quick add reaction.
|
|
@ -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>
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
4
vector/src/main/res/drawable/ic_add_reaction_small.xml
Normal file
4
vector/src/main/res/drawable/ic_add_reaction_small.xml
Normal 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>
|
|
@ -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>
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue