From 440442bb99b45cfb248343f82bb221707ac15d4c Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 5 Jun 2019 19:23:57 +0200 Subject: [PATCH] New View Reactions bottom sheet + visible on reaction long click + Reaction pills size adapt to count, and number format --- .../room/model/relation/RelationService.kt | 3 + .../room/relation/DefaultRelationService.kt | 16 +++ vector/sampledata/reactions.json | 22 ++++ .../riotredesign/core/utils/TextUtils.kt | 29 +++++ .../home/room/detail/RoomDetailFragment.kt | 12 +- .../timeline/TimelineEventController.kt | 1 + .../action/MessageActionsBottomSheet.kt | 15 +-- .../action/MessageActionsViewModel.kt | 3 +- .../timeline/action/MessageMenuFragment.kt | 2 +- .../timeline/action/MessageMenuViewModel.kt | 2 +- .../timeline/action/QuickReactionFragment.kt | 2 +- .../timeline/action/QuickReactionViewModel.kt | 2 +- .../timeline/action/ReactionInfoSimpleItem.kt | 39 +++++++ .../action/TimelineEventFragmentArgs.kt | 12 ++ .../action/ViewReactionBottomSheet.kt | 71 ++++++++++++ .../timeline/action/ViewReactionViewModel.kt | 106 ++++++++++++++++++ .../action/ViewReactionsEpoxyController.kt | 19 ++++ .../detail/timeline/item/AbsMessageItem.kt | 6 +- .../reactions/widget/ReactionButton.kt | 80 +++++++------ .../layout/bottom_sheet_display_reactions.xml | 40 +++++++ .../res/layout/item_simple_reaction_info.xml | 45 ++++++++ .../src/main/res/layout/reaction_button.xml | 32 ++++-- vector/src/main/res/values/strings_riotX.xml | 1 + 23 files changed, 492 insertions(+), 68 deletions(-) create mode 100644 vector/sampledata/reactions.json create mode 100644 vector/src/main/java/im/vector/riotredesign/core/utils/TextUtils.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ReactionInfoSimpleItem.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/TimelineEventFragmentArgs.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt create mode 100644 vector/src/main/res/layout/bottom_sheet_display_reactions.xml create mode 100644 vector/src/main/res/layout/item_simple_reaction_info.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt index bc92472892..ac08b64f53 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt @@ -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> } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index 6d6e4763d2..264909e956 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -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> { + 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. diff --git a/vector/sampledata/reactions.json b/vector/sampledata/reactions.json new file mode 100644 index 0000000000..e2c8e4f4cd --- /dev/null +++ b/vector/sampledata/reactions.json @@ -0,0 +1,22 @@ +{ + "data": [ + { + "reaction" : "👍" + }, + { + "reaction" : "😀" + }, + { + "reaction" : "😞" + }, + { + "reaction" : "Not a reaction" + }, + { + "reaction" : "✅" + }, + { + "reaction" : "🎉" + } + ] +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/utils/TextUtils.kt b/vector/src/main/java/im/vector/riotredesign/core/utils/TextUtils.kt new file mode 100644 index 0000000000..558275565a --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/utils/TextUtils.kt @@ -0,0 +1,29 @@ +package im.vector.riotredesign.core.utils + +import java.util.* + +object TextUtils { + + private val suffixes = TreeMap().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() + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 4653d75166..5ab0e46c94 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -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)) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index 84f970471e..175702cfa8 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -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() diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt index 44a753afbf..fa94043ed4 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 9198f8ee83..a307afe35a 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -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() - val parcel = viewModelContext.args as MessageActionsBottomSheet.ParcelableArgs + val parcel = viewModelContext.args as TimelineEventFragmentArgs val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault()) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuFragment.kt index 3009f5fc67..2b47eae327 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuFragment.kt @@ -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() diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt index 86ddf8665d..776be95042 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt @@ -46,7 +46,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel() - val parcel = viewModelContext.args as MessageActionsBottomSheet.ParcelableArgs + val parcel = viewModelContext.args as TimelineEventFragmentArgs val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId) ?: return null diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionFragment.kt index 2d601544c8..b10744809a 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionFragment.kt @@ -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() diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionViewModel.kt index 36a07bee59..89976248b8 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionViewModel.kt @@ -124,7 +124,7 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel // Args are accessible from the context. // val foo = vieWModelContext.args.foo val currentSession = viewModelContext.activity.get() - 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 diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ReactionInfoSimpleItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ReactionInfoSimpleItem.kt new file mode 100644 index 0000000000..7c7e253167 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ReactionInfoSimpleItem.kt @@ -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() { + + @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(R.id.itemSimpleReactionInfoKey) + val displayNameView by bind(R.id.itemSimpleReactionInfoMemberName) + val timeStampView by bind(R.id.itemSimpleReactionInfoTime) + } + +} diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/TimelineEventFragmentArgs.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/TimelineEventFragmentArgs.kt new file mode 100644 index 0000000000..5764563812 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/TimelineEventFragmentArgs.kt @@ -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 \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt new file mode 100644 index 0000000000..a53dc3bd78 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt @@ -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 } + + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionViewModel.kt new file mode 100644 index 0000000000..a5e7fdd882 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionViewModel.kt @@ -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> = 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>?, + initialState: DisplayReactionsViewState) : VectorViewModel(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() + 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 { + + 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() + val eventId = (viewModelContext.args as TimelineEventFragmentArgs).eventId + return ViewReactionViewModel(session, viewModelContext.activity.get(), viewModelContext.activity, session.getRoom(state.roomId)?.getEventSummaryLive(eventId), state) + } + + + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt new file mode 100644 index 0000000000..a3d146a24f --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt @@ -0,0 +1,19 @@ +package im.vector.riotredesign.features.home.room.detail.timeline.action + +import com.airbnb.epoxy.TypedEpoxyController + + +class ViewReactionsEpoxyController : TypedEpoxyController() { + + 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) + } + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt index b74f7bcac5..a3f43baf51 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -65,6 +65,10 @@ abstract class AbsMessageItem : BaseEventItem() { 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 : BaseEventItem() { //clear all reaction buttons (but not the Flow helper!) holder.reactionWrapper?.children?.forEach { (it as? ReactionButton)?.isGone = true } val idToRefInFlow = ArrayList() - 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 diff --git a/vector/src/main/java/im/vector/riotredesign/features/reactions/widget/ReactionButton.kt b/vector/src/main/java/im/vector/riotredesign/features/reactions/widget/ReactionButton.kt index b0bf08e383..c0716cf28e 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/reactions/widget/ReactionButton.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/reactions/widget/ReactionButton.kt @@ -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) } } \ No newline at end of file diff --git a/vector/src/main/res/layout/bottom_sheet_display_reactions.xml b/vector/src/main/res/layout/bottom_sheet_display_reactions.xml new file mode 100644 index 0000000000..0f5b63654d --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_display_reactions.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_simple_reaction_info.xml b/vector/src/main/res/layout/item_simple_reaction_info.xml new file mode 100644 index 0000000000..0b84aedcd1 --- /dev/null +++ b/vector/src/main/res/layout/item_simple_reaction_info.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/reaction_button.xml b/vector/src/main/res/layout/reaction_button.xml index aca0d2bf7d..6c16929788 100644 --- a/vector/src/main/res/layout/reaction_button.xml +++ b/vector/src/main/res/layout/reaction_button.xml @@ -2,16 +2,19 @@ - + + + + + + tools:text="13450" /> diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 3554d61948..8109c89d10 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -20,6 +20,7 @@ Agree Like Add Reaction + Reactions Event deleted by user Event moderated by room admin