New View Reactions bottom sheet

+ visible on reaction long click
+ Reaction pills size adapt to count, and number format
This commit is contained in:
Valere 2019-06-05 19:23:57 +02:00
parent d2f648edec
commit 440442bb99
23 changed files with 492 additions and 68 deletions

View File

@ -15,7 +15,9 @@
*/
package im.vector.matrix.android.api.session.room.model.relation

import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.util.Cancelable

/**
@ -91,4 +93,5 @@ interface RelationService {
*/
fun replyToMessage(eventReplied: Event, replyText: String): Cancelable?

fun getEventSummaryLive(eventId: String): LiveData<List<EventAnnotationsSummary>>
}

View File

@ -15,15 +15,19 @@
*/
package im.vector.matrix.android.internal.session.room.relation

import androidx.lifecycle.LiveData
import androidx.work.OneTimeWorkRequest
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.relation.RelationService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.helper.addSendingEvent
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.where
@ -169,6 +173,18 @@ internal class DefaultRelationService(private val roomId: String,
return CancelableWork(workRequest.id)
}


override fun getEventSummaryLive(eventId: String): LiveData<List<EventAnnotationsSummary>> {
return monarchy.findAllMappedWithChanges(
{ realm ->
EventAnnotationsSummaryEntity.where(realm, eventId)
},
{
it.asDomain()
}
)
}

/**
* Saves the event in database as a local echo.
* SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room.

View File

@ -0,0 +1,22 @@
{
"data": [
{
"reaction" : "👍"
},
{
"reaction" : "😀"
},
{
"reaction" : "😞"
},
{
"reaction" : "Not a reaction"
},
{
"reaction" : "✅"
},
{
"reaction" : "🎉"
}
]
}

View File

@ -0,0 +1,29 @@
package im.vector.riotredesign.core.utils

import java.util.*

object TextUtils {

private val suffixes = TreeMap<Int, String>().also {
it.put(1000, "k")
it.put(1000000, "M")
it.put(1000000000, "G")
}

fun formatCountToShortDecimal(value: Int): String {
try {
if (value < 0) return "-" + formatCountToShortDecimal(-value)
if (value < 1000) return value.toString() //deal with easy case

val e = suffixes.floorEntry(value)
val divideBy = e.key
val suffix = e.value

val truncated = value / (divideBy!! / 10) //the number part of the output times 10
val hasDecimal = truncated < 100 && truncated / 10.0 != (truncated / 10).toDouble()
return if (hasDecimal) "${truncated / 10.0}$suffix" else "${truncated / 10}$suffix"
} catch (t: Throwable) {
return value.toString()
}
}
}

View File

@ -84,6 +84,7 @@ import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventCo
import im.vector.riotredesign.features.home.room.detail.timeline.action.ActionsHandler
import im.vector.riotredesign.features.home.room.detail.timeline.action.MessageActionsBottomSheet
import im.vector.riotredesign.features.home.room.detail.timeline.action.MessageMenuViewModel
import im.vector.riotredesign.features.home.room.detail.timeline.action.ViewReactionBottomSheet
import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotredesign.features.html.PillImageSpan
@ -235,11 +236,13 @@ class RoomDetailFragment :
var formattedBody: CharSequence? = null
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
val parser = Parser.builder().build()
val document = parser.parse(messageContent.formattedBody ?: messageContent.body)
val document = parser.parse(messageContent.formattedBody
?: messageContent.body)
formattedBody = Markwon.builder(requireContext())
.usePlugin(HtmlPlugin.create()).build().render(document)
}
composerLayout.composerRelatedMessageContent.text = formattedBody ?: nonFormattedBody
composerLayout.composerRelatedMessageContent.text = formattedBody
?: nonFormattedBody


if (mode == SendMode.EDIT) {
@ -593,6 +596,11 @@ class RoomDetailFragment :
}
}

override fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) {
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
}

override fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?) {
editAggregatedSummary?.also {
roomDetailViewModel.process(RoomDetailActions.ShowEditHistoryAction(informationData.eventId, it))

View File

@ -62,6 +62,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,

interface ReactionPillCallback {
fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean)
fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String)
}

private val collapsedEventIds = linkedSetOf<String>()

View File

@ -17,7 +17,6 @@ package im.vector.riotredesign.features.home.room.detail.timeline.action

import android.app.Dialog
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -36,7 +35,6 @@ import im.vector.riotredesign.R
import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
import kotlinx.android.parcel.Parcelize

/**
* Bottom sheet fragment that shows a message preview with list of contextual actions
@ -74,7 +72,7 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() {
val cfm = childFragmentManager
var menuActionFragment = cfm.findFragmentByTag("MenuActionFragment") as? MessageMenuFragment
if (menuActionFragment == null) {
menuActionFragment = MessageMenuFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as ParcelableArgs)
menuActionFragment = MessageMenuFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as TimelineEventFragmentArgs)
cfm.beginTransaction()
.replace(R.id.bottom_sheet_menu_container, menuActionFragment, "MenuActionFragment")
.commit()
@ -89,7 +87,7 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() {

var quickReactionFragment = cfm.findFragmentByTag("QuickReaction") as? QuickReactionFragment
if (quickReactionFragment == null) {
quickReactionFragment = QuickReactionFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as ParcelableArgs)
quickReactionFragment = QuickReactionFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as TimelineEventFragmentArgs)
cfm.beginTransaction()
.replace(R.id.bottom_sheet_quick_reaction_container, quickReactionFragment, "QuickReaction")
.commit()
@ -135,18 +133,11 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() {
}


@Parcelize
data class ParcelableArgs(
val eventId: String,
val roomId: String,
val informationData: MessageInformationData
) : Parcelable

companion object {
fun newInstance(roomId: String, informationData: MessageInformationData): MessageActionsBottomSheet {
return MessageActionsBottomSheet().apply {
setArguments(
ParcelableArgs(
TimelineEventFragmentArgs(
informationData.eventId,
roomId,
informationData

View File

@ -25,7 +25,6 @@ import im.vector.matrix.android.api.session.room.model.message.MessageTextConten
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.riotredesign.core.platform.VectorViewModel
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import org.koin.android.ext.android.get
import ru.noties.markwon.Markwon
import ru.noties.markwon.html.HtmlPlugin
@ -51,7 +50,7 @@ class MessageActionsViewModel(initialState: MessageActionState) : VectorViewMode

override fun initialState(viewModelContext: ViewModelContext): MessageActionState? {
val currentSession = viewModelContext.activity.get<Session>()
val parcel = viewModelContext.args as MessageActionsBottomSheet.ParcelableArgs
val parcel = viewModelContext.args as TimelineEventFragmentArgs

val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault())


View File

@ -101,7 +101,7 @@ class MessageMenuFragment : BaseMvRxFragment() {


companion object {
fun newInstance(pa: MessageActionsBottomSheet.ParcelableArgs): MessageMenuFragment {
fun newInstance(pa: TimelineEventFragmentArgs): MessageMenuFragment {
val args = Bundle()
args.putParcelable(MvRx.KEY_ARG, pa)
val fragment = MessageMenuFragment()

View File

@ -46,7 +46,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
override fun initialState(viewModelContext: ViewModelContext): MessageMenuState? {
// Args are accessible from the context.
val currentSession = viewModelContext.activity.get<Session>()
val parcel = viewModelContext.args as MessageActionsBottomSheet.ParcelableArgs
val parcel = viewModelContext.args as TimelineEventFragmentArgs
val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId)
?: return null


View File

@ -139,7 +139,7 @@ class QuickReactionFragment : BaseMvRxFragment() {
}

companion object {
fun newInstance(pa: MessageActionsBottomSheet.ParcelableArgs): QuickReactionFragment {
fun newInstance(pa: TimelineEventFragmentArgs): QuickReactionFragment {
val args = Bundle()
args.putParcelable(MvRx.KEY_ARG, pa)
val fragment = QuickReactionFragment()

View File

@ -124,7 +124,7 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel
// Args are accessible from the context.
// val foo = vieWModelContext.args<MyArgs>.foo
val currentSession = viewModelContext.activity.get<Session>()
val parcel = viewModelContext.args as MessageActionsBottomSheet.ParcelableArgs
val parcel = viewModelContext.args as TimelineEventFragmentArgs
val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId)
?: return null
var agreeTriggle: TriggleState = TriggleState.NONE

View File

@ -0,0 +1,39 @@
package im.vector.riotredesign.features.home.room.detail.timeline.action

import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder


@EpoxyModelClass(layout = R.layout.item_simple_reaction_info)
abstract class ReactionInfoSimpleItem : EpoxyModelWithHolder<ReactionInfoSimpleItem.Holder>() {

@EpoxyAttribute
lateinit var reactionKey: CharSequence
@EpoxyAttribute
lateinit var authorDisplayName: CharSequence
@EpoxyAttribute
var timeStamp: CharSequence? = null

override fun bind(holder: Holder) {
holder.titleView.text = reactionKey
holder.displayNameView.text = authorDisplayName
timeStamp?.let {
holder.timeStampView.text = it
holder.timeStampView.isVisible = true
} ?: run {
holder.timeStampView.isVisible = false
}
}

class Holder : VectorEpoxyHolder() {
val titleView by bind<TextView>(R.id.itemSimpleReactionInfoKey)
val displayNameView by bind<TextView>(R.id.itemSimpleReactionInfoMemberName)
val timeStampView by bind<TextView>(R.id.itemSimpleReactionInfoTime)
}

}

View File

@ -0,0 +1,12 @@
package im.vector.riotredesign.features.home.room.detail.timeline.action

import android.os.Parcelable
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
import kotlinx.android.parcel.Parcelize

@Parcelize
data class TimelineEventFragmentArgs(
val eventId: String,
val roomId: String,
val informationData: MessageInformationData
) : Parcelable

View File

@ -0,0 +1,71 @@
package im.vector.riotredesign.features.home.room.detail.timeline.action

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DividerItemDecoration
import butterknife.BindView
import butterknife.ButterKnife
import com.airbnb.epoxy.EpoxyRecyclerView
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotredesign.R
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
import kotlinx.android.synthetic.main.bottom_sheet_display_reactions.*


class ViewReactionBottomSheet : BaseMvRxBottomSheetDialog() {

private val viewModel: ViewReactionViewModel by fragmentViewModel(ViewReactionViewModel::class)

private val eventArgs: TimelineEventFragmentArgs by args()

@BindView(R.id.bottom_sheet_display_reactions_list)
lateinit var epoxyRecyclerView: EpoxyRecyclerView

private val epoxyController by lazy { ViewReactionsEpoxyController() }

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.bottom_sheet_display_reactions, container, false)
ButterKnife.bind(this, view)
return view
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
epoxyRecyclerView.setController(epoxyController)
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
LinearLayout.VERTICAL)
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
}


override fun invalidate() = withState(viewModel) {
if (it.mapReactionKeyToMemberList() == null) {
bottomSheetViewReactionSpinner.isVisible = true
bottomSheetViewReactionSpinner.animate()
} else {
bottomSheetViewReactionSpinner.isVisible = false
}
epoxyController.setData(it)
}

companion object {
fun newInstance(roomId: String, informationData: MessageInformationData): ViewReactionBottomSheet {
val args = Bundle()
val parcelableArgs = TimelineEventFragmentArgs(
informationData.eventId,
roomId,
informationData
)
args.putParcelable(MvRx.KEY_ARG, parcelableArgs)
return ViewReactionBottomSheet().apply { arguments = args }

}
}
}

View File

@ -0,0 +1,106 @@
package im.vector.riotredesign.features.home.room.detail.timeline.action

import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import com.airbnb.mvrx.*
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.koin.android.ext.android.get


data class DisplayReactionsViewState(
val eventId: String = "",
val roomId: String = "",
val mapReactionKeyToMemberList: Async<List<ReactionInfo>> = Uninitialized)
: MvRxState

data class ReactionInfo(
val eventId: String,
val reactionKey: String,
val authorId: String,
val authorName: String? = null,
val timestamp: String? = null
)

/**
* Used to display the list of members that reacted to a given event
*/
class ViewReactionViewModel(private val session: Session,
private val timelineDateFormatter: TimelineDateFormatter,
lifecycleOwner: LifecycleOwner?,
liveSummary: LiveData<List<EventAnnotationsSummary>>?,
initialState: DisplayReactionsViewState) : VectorViewModel<DisplayReactionsViewState>(initialState) {

init {
loadReaction()
if (lifecycleOwner != null) {
liveSummary?.observe(lifecycleOwner, Observer {
it?.firstOrNull()?.let {
loadReaction()
}
})
}

}

private fun loadReaction() = withState { state ->

GlobalScope.launch {
try {
val room = session.getRoom(state.roomId)
val event = room?.getTimeLineEvent(state.eventId)
if (event == null) {
setState { copy(mapReactionKeyToMemberList = Fail(Throwable())) }
return@launch
}
var results = ArrayList<ReactionInfo>()
event.annotations?.reactionsSummary?.forEach { sum ->

sum.sourceEvents.mapNotNull { room.getTimeLineEvent(it) }.forEach {
val localDate = it.root.localDateTime()
results.add(ReactionInfo(it.root.eventId!!, sum.key, it.root.sender
?: "", it.senderName, timelineDateFormatter.formatMessageHour(localDate)))
}
}
setState {
copy(
mapReactionKeyToMemberList = Success(results.sortedBy { it.timestamp })
)
}
} catch (t: Throwable) {
setState {
copy(
mapReactionKeyToMemberList = Fail(t)
)
}
}
}
}


companion object : MvRxViewModelFactory<ViewReactionViewModel, DisplayReactionsViewState> {

override fun initialState(viewModelContext: ViewModelContext): DisplayReactionsViewState? {

val roomId = (viewModelContext.args as? TimelineEventFragmentArgs)?.roomId
?: return null
val info = (viewModelContext.args as? TimelineEventFragmentArgs)?.informationData
?: return null
return DisplayReactionsViewState(info.eventId, roomId)
}

override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionViewModel? {
val session = viewModelContext.activity.get<Session>()
val eventId = (viewModelContext.args as TimelineEventFragmentArgs).eventId
return ViewReactionViewModel(session, viewModelContext.activity.get(), viewModelContext.activity, session.getRoom(state.roomId)?.getEventSummaryLive(eventId), state)
}


}
}

View File

@ -0,0 +1,19 @@
package im.vector.riotredesign.features.home.room.detail.timeline.action

import com.airbnb.epoxy.TypedEpoxyController


class ViewReactionsEpoxyController : TypedEpoxyController<DisplayReactionsViewState>() {

override fun buildModels(state: DisplayReactionsViewState) {
val map = state.mapReactionKeyToMemberList() ?: return
map.forEach {
reactionInfoSimpleItem {
id(it.eventId)
timeStamp(it.timestamp)
reactionKey(it.reactionKey)
authorDisplayName(it.authorName ?: it.authorId)
}
}
}
}

View File

@ -65,6 +65,10 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
override fun onUnReacted(reactionButton: ReactionButton) {
reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString, false)
}

override fun onLongClick(reactionButton: ReactionButton) {
reactionPillCallback?.onLongClickOnReactionPill(informationData, reactionButton.reactionString)
}
}

override fun bind(holder: H) {
@ -112,7 +116,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
//clear all reaction buttons (but not the Flow helper!)
holder.reactionWrapper?.children?.forEach { (it as? ReactionButton)?.isGone = true }
val idToRefInFlow = ArrayList<Int>()
informationData.orderedReactionList?.chunked(7)?.firstOrNull()?.forEachIndexed { index, reaction ->
informationData.orderedReactionList?.chunked(8)?.firstOrNull()?.forEachIndexed { index, reaction ->
(holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton ->
reactionButton.isVisible = true
reactionButton.reactedListener = reactionClickListener

View File

@ -37,13 +37,14 @@ import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import im.vector.riotredesign.R
import im.vector.riotredesign.core.utils.TextUtils

/**
* An animated reaction button.
* Displays a String reaction (emoji), with a count, and that can be selected or not (toggle)
*/
class ReactionButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr), View.OnClickListener {
defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr), View.OnClickListener, View.OnLongClickListener {

companion object {
private val DECCELERATE_INTERPOLATOR = DecelerateInterpolator()
@ -74,7 +75,7 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
var reactionCount = 11
set(value) {
field = value
countTextView?.text = value.toString()
countTextView?.text = TextUtils.formatCountToShortDecimal(value)
}


@ -101,7 +102,7 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
reactionSelector = findViewById(R.id.reactionSelector)
countTextView = findViewById(R.id.reactionCount)

countTextView?.text = reactionCount.toString()
countTextView?.text = TextUtils.formatCountToShortDecimal(reactionCount)

emojiView?.typeface = this.emojiTypeFace ?: Typeface.DEFAULT

@ -136,6 +137,7 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
val status = array.getBoolean(R.styleable.ReactionButton_toggled, false)
setChecked(status)
setOnClickListener(this)
setOnLongClickListener(this)
array.recycle()
}

@ -242,40 +244,45 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
* @param event
* @return
*/
override fun onTouchEvent(event: MotionEvent): Boolean {
if (!isEnabled)
return true
// override fun onTouchEvent(event: MotionEvent): Boolean {
// if (!isEnabled)
// return true
//
// when (event.action) {
// MotionEvent.ACTION_DOWN ->
// /*
// Commented out this line and moved the animation effect to the action up event due to
// conflicts that were occurring when library is used in sliding type views.
//
// icon.animate().scaleX(0.7f).scaleY(0.7f).setDuration(150).setInterpolator(DECCELERATE_INTERPOLATOR);
// */
// isPressed = true
//
// MotionEvent.ACTION_MOVE -> {
// val x = event.x
// val y = event.y
// val isInside = x > 0 && x < width && y > 0 && y < height
// if (isPressed != isInside) {
// isPressed = isInside
// }
// }
//
// MotionEvent.ACTION_UP -> {
// emojiView!!.animate().scaleX(0.7f).scaleY(0.7f).setDuration(150).interpolator = DECCELERATE_INTERPOLATOR
// emojiView!!.animate().scaleX(1f).scaleY(1f).interpolator = DECCELERATE_INTERPOLATOR
// if (isPressed) {
// performClick()
// isPressed = false
// }
// }
// MotionEvent.ACTION_CANCEL -> isPressed = false
// }
// return true
// }

when (event.action) {
MotionEvent.ACTION_DOWN ->
/*
Commented out this line and moved the animation effect to the action up event due to
conflicts that were occurring when library is used in sliding type views.

icon.animate().scaleX(0.7f).scaleY(0.7f).setDuration(150).setInterpolator(DECCELERATE_INTERPOLATOR);
*/
isPressed = true

MotionEvent.ACTION_MOVE -> {
val x = event.x
val y = event.y
val isInside = x > 0 && x < width && y > 0 && y < height
if (isPressed != isInside) {
isPressed = isInside
}
}

MotionEvent.ACTION_UP -> {
emojiView!!.animate().scaleX(0.7f).scaleY(0.7f).setDuration(150).interpolator = DECCELERATE_INTERPOLATOR
emojiView!!.animate().scaleX(1f).scaleY(1f).interpolator = DECCELERATE_INTERPOLATOR
if (isPressed) {
performClick()
isPressed = false
}
}
MotionEvent.ACTION_CANCEL -> isPressed = false
}
return true
override fun onLongClick(v: View?): Boolean {
reactedListener?.onLongClick(this)
return reactedListener != null
}

/**
@ -335,5 +342,6 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
interface ReactedListener {
fun onReacted(reactionButton: ReactionButton)
fun onUnReacted(reactionButton: ReactionButton)
fun onLongClick(reactionButton: ReactionButton)
}
}

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="400dp"
android:orientation="vertical">

<TextView
android:layout_width="match_parent"
android:layout_height="44dp"
android:gravity="center_vertical"
android:padding="8dp"
android:text="@string/reactions"
android:textColor="?android:textColorSecondary"
android:textSize="16sp" />

<ProgressBar
android:id="@+id/bottomSheetViewReactionSpinner"
style="?android:attr/progressBarStyleSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:visibility="gone"
tools:visibility="visible" />


<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/bottom_sheet_display_reactions_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fadeScrollbars="false"
android:orientation="vertical"
android:scrollbars="vertical"
tools:itemCount="15"
tools:listitem="@layout/item_simple_reaction_info">

</com.airbnb.epoxy.EpoxyRecyclerView>
</LinearLayout>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="44dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="8dp"
android:paddingLeft="8dp"
android:paddingEnd="8dp">

<TextView
android:id="@+id/itemSimpleReactionInfoKey"
android:layout_width="44dp"
android:layout_height="wrap_content"
android:gravity="center"
android:lines="1"
android:textColor="?android:textColorPrimary"
android:textSize="18sp"
tools:text="@sample/reactions.json/data/reaction" />

<TextView
android:id="@+id/itemSimpleReactionInfoMemberName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:layout_weight="1"
android:ellipsize="end"
android:lines="1"
android:textColor="?android:textColorPrimary"
android:textSize="16sp"
tools:text="@sample/matrix.json/data/displayName" />

<TextView
android:id="@+id/itemSimpleReactionInfoTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lines="1"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
tools:text="10:44" />


</LinearLayout>

View File

@ -2,16 +2,19 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="44dp"
android:id="@+id/reactionSelector"
android:layout_width="wrap_content"
android:minWidth="44dp"
android:layout_height="26dp"
android:background="@drawable/rounded_rect_shape"
android:clipChildren="false">


<View
android:id="@+id/reactionSelector"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/rounded_rect_shape" />
<!--<View-->
<!--android:id="@+id/reactionSelector"-->
<!--android:layout_width="match_parent"-->
<!--android:layout_height="match_parent"-->
<!--android:background="@drawable/rounded_rect_shape" />-->

<im.vector.riotredesign.features.reactions.widget.DotsView
android:id="@+id/dots"
@ -42,17 +45,23 @@
android:gravity="center"
android:textColor="@color/black"
android:textSize="13sp"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/reactionCount"
tools:text="👍" />

<TextView
android:id="@+id/reactionCount"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginEnd="6dp"
android:layout_marginRight="6dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintBaseline_toBaselineOf="@id/reactionText"
android:layout_marginStart="-4dp"
android:layout_marginLeft="-4dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:gravity="center"
android:maxLines="1"
android:textColor="?riotx_text_secondary"
@ -61,7 +70,8 @@
app:autoSizeMaxTextSize="14sp"
app:autoSizeMinTextSize="8sp"
app:autoSizeTextType="uniform"
app:layout_constraintStart_toEndOf="@id/reactionText"
app:layout_constraintEnd_toEndOf="parent"
tools:text="10" />
tools:text="13450" />

</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -20,6 +20,7 @@
<string name="reactions_agree">Agree</string>
<string name="reactions_like">Like</string>
<string name="message_add_reaction">Add Reaction</string>
<string name="reactions">Reactions</string>

<string name="event_redacted_by_user_reason">Event deleted by user</string>
<string name="event_redacted_by_admin_reason">Event moderated by room admin</string>