mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-27 03:49:04 +03:00
New View Reactions bottom sheet
+ visible on reaction long click + Reaction pills size adapt to count, and number format
This commit is contained in:
parent
d2f648edec
commit
440442bb99
23 changed files with 492 additions and 68 deletions
|
@ -15,7 +15,9 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.matrix.android.api.session.room.model.relation
|
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.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -91,4 +93,5 @@ interface RelationService {
|
||||||
*/
|
*/
|
||||||
fun replyToMessage(eventReplied: Event, replyText: String): Cancelable?
|
fun replyToMessage(eventReplied: Event, replyText: String): Cancelable?
|
||||||
|
|
||||||
|
fun getEventSummaryLive(eventId: String): LiveData<List<EventAnnotationsSummary>>
|
||||||
}
|
}
|
|
@ -15,15 +15,19 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.matrix.android.internal.session.room.relation
|
package im.vector.matrix.android.internal.session.room.relation
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.work.OneTimeWorkRequest
|
import androidx.work.OneTimeWorkRequest
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
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.message.MessageType
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.RelationService
|
import im.vector.matrix.android.api.session.room.model.relation.RelationService
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
import im.vector.matrix.android.internal.database.helper.addSendingEvent
|
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.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.model.RoomEntity
|
||||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
@ -169,6 +173,18 @@ internal class DefaultRelationService(private val roomId: String,
|
||||||
return CancelableWork(workRequest.id)
|
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.
|
* 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.
|
* SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room.
|
||||||
|
|
22
vector/sampledata/reactions.json
Normal file
22
vector/sampledata/reactions.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"reaction" : "👍"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reaction" : "😀"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reaction" : "😞"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reaction" : "Not a reaction"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reaction" : "✅"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reaction" : "🎉"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.ActionsHandler
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.action.MessageActionsBottomSheet
|
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.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.helper.EndlessRecyclerViewScrollListener
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
|
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
|
||||||
import im.vector.riotredesign.features.html.PillImageSpan
|
import im.vector.riotredesign.features.html.PillImageSpan
|
||||||
|
@ -235,11 +236,13 @@ class RoomDetailFragment :
|
||||||
var formattedBody: CharSequence? = null
|
var formattedBody: CharSequence? = null
|
||||||
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||||
val parser = Parser.builder().build()
|
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())
|
formattedBody = Markwon.builder(requireContext())
|
||||||
.usePlugin(HtmlPlugin.create()).build().render(document)
|
.usePlugin(HtmlPlugin.create()).build().render(document)
|
||||||
}
|
}
|
||||||
composerLayout.composerRelatedMessageContent.text = formattedBody ?: nonFormattedBody
|
composerLayout.composerRelatedMessageContent.text = formattedBody
|
||||||
|
?: nonFormattedBody
|
||||||
|
|
||||||
|
|
||||||
if (mode == SendMode.EDIT) {
|
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?) {
|
override fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?) {
|
||||||
editAggregatedSummary?.also {
|
editAggregatedSummary?.also {
|
||||||
roomDetailViewModel.process(RoomDetailActions.ShowEditHistoryAction(informationData.eventId, it))
|
roomDetailViewModel.process(RoomDetailActions.ShowEditHistoryAction(informationData.eventId, it))
|
||||||
|
|
|
@ -62,6 +62,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
||||||
|
|
||||||
interface ReactionPillCallback {
|
interface ReactionPillCallback {
|
||||||
fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean)
|
fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean)
|
||||||
|
fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val collapsedEventIds = linkedSetOf<String>()
|
private val collapsedEventIds = linkedSetOf<String>()
|
||||||
|
|
|
@ -17,7 +17,6 @@ package im.vector.riotredesign.features.home.room.detail.timeline.action
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -36,7 +35,6 @@ import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.glide.GlideApp
|
import im.vector.riotredesign.core.glide.GlideApp
|
||||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
|
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
|
* Bottom sheet fragment that shows a message preview with list of contextual actions
|
||||||
|
@ -74,7 +72,7 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() {
|
||||||
val cfm = childFragmentManager
|
val cfm = childFragmentManager
|
||||||
var menuActionFragment = cfm.findFragmentByTag("MenuActionFragment") as? MessageMenuFragment
|
var menuActionFragment = cfm.findFragmentByTag("MenuActionFragment") as? MessageMenuFragment
|
||||||
if (menuActionFragment == null) {
|
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()
|
cfm.beginTransaction()
|
||||||
.replace(R.id.bottom_sheet_menu_container, menuActionFragment, "MenuActionFragment")
|
.replace(R.id.bottom_sheet_menu_container, menuActionFragment, "MenuActionFragment")
|
||||||
.commit()
|
.commit()
|
||||||
|
@ -89,7 +87,7 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() {
|
||||||
|
|
||||||
var quickReactionFragment = cfm.findFragmentByTag("QuickReaction") as? QuickReactionFragment
|
var quickReactionFragment = cfm.findFragmentByTag("QuickReaction") as? QuickReactionFragment
|
||||||
if (quickReactionFragment == null) {
|
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()
|
cfm.beginTransaction()
|
||||||
.replace(R.id.bottom_sheet_quick_reaction_container, quickReactionFragment, "QuickReaction")
|
.replace(R.id.bottom_sheet_quick_reaction_container, quickReactionFragment, "QuickReaction")
|
||||||
.commit()
|
.commit()
|
||||||
|
@ -135,18 +133,11 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableArgs(
|
|
||||||
val eventId: String,
|
|
||||||
val roomId: String,
|
|
||||||
val informationData: MessageInformationData
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance(roomId: String, informationData: MessageInformationData): MessageActionsBottomSheet {
|
fun newInstance(roomId: String, informationData: MessageInformationData): MessageActionsBottomSheet {
|
||||||
return MessageActionsBottomSheet().apply {
|
return MessageActionsBottomSheet().apply {
|
||||||
setArguments(
|
setArguments(
|
||||||
ParcelableArgs(
|
TimelineEventFragmentArgs(
|
||||||
informationData.eventId,
|
informationData.eventId,
|
||||||
roomId,
|
roomId,
|
||||||
informationData
|
informationData
|
||||||
|
|
|
@ -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.matrix.android.api.session.room.model.message.MessageType
|
||||||
import im.vector.riotredesign.core.platform.VectorViewModel
|
import im.vector.riotredesign.core.platform.VectorViewModel
|
||||||
import org.commonmark.parser.Parser
|
import org.commonmark.parser.Parser
|
||||||
import org.commonmark.renderer.html.HtmlRenderer
|
|
||||||
import org.koin.android.ext.android.get
|
import org.koin.android.ext.android.get
|
||||||
import ru.noties.markwon.Markwon
|
import ru.noties.markwon.Markwon
|
||||||
import ru.noties.markwon.html.HtmlPlugin
|
import ru.noties.markwon.html.HtmlPlugin
|
||||||
|
@ -51,7 +50,7 @@ class MessageActionsViewModel(initialState: MessageActionState) : VectorViewMode
|
||||||
|
|
||||||
override fun initialState(viewModelContext: ViewModelContext): MessageActionState? {
|
override fun initialState(viewModelContext: ViewModelContext): MessageActionState? {
|
||||||
val currentSession = viewModelContext.activity.get<Session>()
|
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())
|
val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault())
|
||||||
|
|
||||||
|
|
|
@ -101,7 +101,7 @@ class MessageMenuFragment : BaseMvRxFragment() {
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance(pa: MessageActionsBottomSheet.ParcelableArgs): MessageMenuFragment {
|
fun newInstance(pa: TimelineEventFragmentArgs): MessageMenuFragment {
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
args.putParcelable(MvRx.KEY_ARG, pa)
|
args.putParcelable(MvRx.KEY_ARG, pa)
|
||||||
val fragment = MessageMenuFragment()
|
val fragment = MessageMenuFragment()
|
||||||
|
|
|
@ -46,7 +46,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
||||||
override fun initialState(viewModelContext: ViewModelContext): MessageMenuState? {
|
override fun initialState(viewModelContext: ViewModelContext): MessageMenuState? {
|
||||||
// Args are accessible from the context.
|
// Args are accessible from the context.
|
||||||
val currentSession = viewModelContext.activity.get<Session>()
|
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)
|
val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId)
|
||||||
?: return null
|
?: return null
|
||||||
|
|
||||||
|
|
|
@ -139,7 +139,7 @@ class QuickReactionFragment : BaseMvRxFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance(pa: MessageActionsBottomSheet.ParcelableArgs): QuickReactionFragment {
|
fun newInstance(pa: TimelineEventFragmentArgs): QuickReactionFragment {
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
args.putParcelable(MvRx.KEY_ARG, pa)
|
args.putParcelable(MvRx.KEY_ARG, pa)
|
||||||
val fragment = QuickReactionFragment()
|
val fragment = QuickReactionFragment()
|
||||||
|
|
|
@ -124,7 +124,7 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel
|
||||||
// Args are accessible from the context.
|
// Args are accessible from the context.
|
||||||
// val foo = vieWModelContext.args<MyArgs>.foo
|
// val foo = vieWModelContext.args<MyArgs>.foo
|
||||||
val currentSession = viewModelContext.activity.get<Session>()
|
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)
|
val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId)
|
||||||
?: return null
|
?: return null
|
||||||
var agreeTriggle: TriggleState = TriggleState.NONE
|
var agreeTriggle: TriggleState = TriggleState.NONE
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
|
@ -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 }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -65,6 +65,10 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||||
override fun onUnReacted(reactionButton: ReactionButton) {
|
override fun onUnReacted(reactionButton: ReactionButton) {
|
||||||
reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString, false)
|
reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onLongClick(reactionButton: ReactionButton) {
|
||||||
|
reactionPillCallback?.onLongClickOnReactionPill(informationData, reactionButton.reactionString)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(holder: H) {
|
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!)
|
//clear all reaction buttons (but not the Flow helper!)
|
||||||
holder.reactionWrapper?.children?.forEach { (it as? ReactionButton)?.isGone = true }
|
holder.reactionWrapper?.children?.forEach { (it as? ReactionButton)?.isGone = true }
|
||||||
val idToRefInFlow = ArrayList<Int>()
|
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 ->
|
(holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton ->
|
||||||
reactionButton.isVisible = true
|
reactionButton.isVisible = true
|
||||||
reactionButton.reactedListener = reactionClickListener
|
reactionButton.reactedListener = reactionClickListener
|
||||||
|
|
|
@ -37,13 +37,14 @@ import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.ColorRes
|
import androidx.annotation.ColorRes
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.utils.TextUtils
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An animated reaction button.
|
* An animated reaction button.
|
||||||
* Displays a String reaction (emoji), with a count, and that can be selected or not (toggle)
|
* 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,
|
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 {
|
companion object {
|
||||||
private val DECCELERATE_INTERPOLATOR = DecelerateInterpolator()
|
private val DECCELERATE_INTERPOLATOR = DecelerateInterpolator()
|
||||||
|
@ -74,7 +75,7 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
|
||||||
var reactionCount = 11
|
var reactionCount = 11
|
||||||
set(value) {
|
set(value) {
|
||||||
field = 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)
|
reactionSelector = findViewById(R.id.reactionSelector)
|
||||||
countTextView = findViewById(R.id.reactionCount)
|
countTextView = findViewById(R.id.reactionCount)
|
||||||
|
|
||||||
countTextView?.text = reactionCount.toString()
|
countTextView?.text = TextUtils.formatCountToShortDecimal(reactionCount)
|
||||||
|
|
||||||
emojiView?.typeface = this.emojiTypeFace ?: Typeface.DEFAULT
|
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)
|
val status = array.getBoolean(R.styleable.ReactionButton_toggled, false)
|
||||||
setChecked(status)
|
setChecked(status)
|
||||||
setOnClickListener(this)
|
setOnClickListener(this)
|
||||||
|
setOnLongClickListener(this)
|
||||||
array.recycle()
|
array.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,40 +244,45 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
|
||||||
* @param event
|
* @param event
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
// override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
if (!isEnabled)
|
// if (!isEnabled)
|
||||||
return true
|
// 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) {
|
override fun onLongClick(v: View?): Boolean {
|
||||||
MotionEvent.ACTION_DOWN ->
|
reactedListener?.onLongClick(this)
|
||||||
/*
|
return reactedListener != null
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -335,5 +342,6 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
|
||||||
interface ReactedListener {
|
interface ReactedListener {
|
||||||
fun onReacted(reactionButton: ReactionButton)
|
fun onReacted(reactionButton: ReactionButton)
|
||||||
fun onUnReacted(reactionButton: ReactionButton)
|
fun onUnReacted(reactionButton: ReactionButton)
|
||||||
|
fun onLongClick(reactionButton: ReactionButton)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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>
|
45
vector/src/main/res/layout/item_simple_reaction_info.xml
Normal file
45
vector/src/main/res/layout/item_simple_reaction_info.xml
Normal 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>
|
|
@ -2,16 +2,19 @@
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
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="44dp"
|
android:id="@+id/reactionSelector"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:minWidth="44dp"
|
||||||
android:layout_height="26dp"
|
android:layout_height="26dp"
|
||||||
|
android:background="@drawable/rounded_rect_shape"
|
||||||
android:clipChildren="false">
|
android:clipChildren="false">
|
||||||
|
|
||||||
|
|
||||||
<View
|
<!--<View-->
|
||||||
android:id="@+id/reactionSelector"
|
<!--android:id="@+id/reactionSelector"-->
|
||||||
android:layout_width="match_parent"
|
<!--android:layout_width="match_parent"-->
|
||||||
android:layout_height="match_parent"
|
<!--android:layout_height="match_parent"-->
|
||||||
android:background="@drawable/rounded_rect_shape" />
|
<!--android:background="@drawable/rounded_rect_shape" />-->
|
||||||
|
|
||||||
<im.vector.riotredesign.features.reactions.widget.DotsView
|
<im.vector.riotredesign.features.reactions.widget.DotsView
|
||||||
android:id="@+id/dots"
|
android:id="@+id/dots"
|
||||||
|
@ -42,17 +45,23 @@
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:textColor="@color/black"
|
android:textColor="@color/black"
|
||||||
android:textSize="13sp"
|
android:textSize="13sp"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/reactionCount"
|
||||||
tools:text="👍" />
|
tools:text="👍" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/reactionCount"
|
android:id="@+id/reactionCount"
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="6dp"
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
android:layout_marginRight="6dp"
|
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:gravity="center"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textColor="?riotx_text_secondary"
|
android:textColor="?riotx_text_secondary"
|
||||||
|
@ -61,7 +70,8 @@
|
||||||
app:autoSizeMaxTextSize="14sp"
|
app:autoSizeMaxTextSize="14sp"
|
||||||
app:autoSizeMinTextSize="8sp"
|
app:autoSizeMinTextSize="8sp"
|
||||||
app:autoSizeTextType="uniform"
|
app:autoSizeTextType="uniform"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/reactionText"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
tools:text="10" />
|
tools:text="13450" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
<string name="reactions_agree">Agree</string>
|
<string name="reactions_agree">Agree</string>
|
||||||
<string name="reactions_like">Like</string>
|
<string name="reactions_like">Like</string>
|
||||||
<string name="message_add_reaction">Add Reaction</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_user_reason">Event deleted by user</string>
|
||||||
<string name="event_redacted_by_admin_reason">Event moderated by room admin</string>
|
<string name="event_redacted_by_admin_reason">Event moderated by room admin</string>
|
||||||
|
|
Loading…
Reference in a new issue