Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/fga/reactions UI improvements #5204

Merged
merged 6 commits into from
Feb 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/5204.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improve UI of reactions in timeline, including quick add reaction.
11 changes: 11 additions & 0 deletions library/ui-styles/src/main/res/values/styles_timeline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,15 @@
<item name="android:backgroundTint">?vctr_content_quinary</item>
</style>

<style name="TimelineReactionView">
<item name="android:paddingStart">6dp</item>
<item name="android:paddingEnd">6dp</item>
<item name="android:paddingTop">1dp</item>
<item name="android:paddingBottom">1dp</item>
<item name="android:minHeight">28dp</item>
<item name="android:minWidth">40dp</item>
<item name="android:gravity">center</item>
</style>


</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@ internal class UIEchoManager(private val listener: Listener) {
return existingState != sendState
}

fun onLocalEchoCreated(timelineEvent: TimelineEvent): Boolean {
fun onLocalEchoCreated(timelineEvent: TimelineEvent): Boolean {
when (timelineEvent.root.getClearType()) {
EventType.REDACTION -> {
}
EventType.REACTION -> {
EventType.REACTION -> {
val content: ReactionContent? = timelineEvent.root.content?.toModel<ReactionContent>()
if (RelationType.ANNOTATION == content?.relatesTo?.type) {
val reaction = content.relatesTo.key
Expand Down Expand Up @@ -104,8 +104,8 @@ internal class UIEchoManager(private val listener: Listener) {
val updateReactions = existingAnnotationSummary.reactionsSummary.toMutableList()

contents.forEach { uiEchoReaction ->
val existing = updateReactions.firstOrNull { it.key == uiEchoReaction.reaction }
if (existing == null) {
val indexOfExistingReaction = updateReactions.indexOfFirst { it.key == uiEchoReaction.reaction }
if (indexOfExistingReaction == -1) {
// just add the new key
ReactionAggregatedSummary(
key = uiEchoReaction.reaction,
Expand All @@ -117,6 +117,7 @@ internal class UIEchoManager(private val listener: Listener) {
).let { updateReactions.add(it) }
} else {
// update Existing Key
val existing = updateReactions[indexOfExistingReaction]
if (!existing.localEchoEvents.contains(uiEchoReaction.localEchoId)) {
updateReactions.remove(existing)
// only update if echo is not yet there
Expand All @@ -128,7 +129,7 @@ internal class UIEchoManager(private val listener: Listener) {
sourceEvents = existing.sourceEvents,
localEchoEvents = existing.localEchoEvents + uiEchoReaction.localEchoId

).let { updateReactions.add(it) }
).let { updateReactions.add(indexOfExistingReaction, it) }
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions vector/src/main/java/im/vector/app/core/extensions/Context.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ package im.vector.app.core.extensions

import android.content.Context
import android.graphics.drawable.Drawable
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ImageSpan
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
Expand All @@ -34,6 +37,16 @@ fun Context.singletonEntryPoint(): SingletonEntryPoint {
return EntryPoints.get(applicationContext, SingletonEntryPoint::class.java)
}

fun Context.getDrawableAsSpannable(@DrawableRes drawableRes: Int, alignment: Int = ImageSpan.ALIGN_BOTTOM): Spannable {
return SpannableString(" ").apply {
val span = ContextCompat.getDrawable(this@getDrawableAsSpannable, drawableRes)?.let {
it.setBounds(0, 0, it.intrinsicWidth, it.intrinsicHeight)
ImageSpan(it, alignment)
}
setSpan(span, 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
}
}

fun Context.getResTintedDrawable(@DrawableRes drawableRes: Int, @ColorRes tint: Int, @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1f): Drawable? {
return getTintedDrawable(drawableRes, ContextCompat.getColor(this, tint), alpha)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1913,6 +1913,10 @@ class TimelineFragment @Inject constructor(
timelineViewModel.handle(RoomDetailAction.LoadMoreTimelineEvents(direction))
}

override fun onAddMoreReaction(event: TimelineEvent) {
openEmojiReactionPicker(event.eventId)
}

override fun onEventCellClicked(informationData: MessageInformationData, messageContent: Any?, view: View, isRootThreadEvent: Boolean) {
when (messageContent) {
is MessageVerificationRequestContent -> {
Expand Down Expand Up @@ -2117,7 +2121,7 @@ class TimelineFragment @Inject constructor(
openRoomMemberProfile(action.userId)
}
is EventSharedAction.AddReaction -> {
emojiActivityResultLauncher.launch(EmojiReactionPickerActivity.intent(requireContext(), action.eventId))
openEmojiReactionPicker(action.eventId)
}
is EventSharedAction.ViewReactions -> {
ViewReactionsBottomSheet.newInstance(timelineArgs.roomId, action.messageInformationData)
Expand Down Expand Up @@ -2239,6 +2243,10 @@ class TimelineFragment @Inject constructor(
}
}

private fun openEmojiReactionPicker(eventId: String) {
emojiActivityResultLauncher.launch(EmojiReactionPickerActivity.intent(requireContext(), eventId))
}

private fun askConfirmationToEndPoll(eventId: String) {
MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Vector_MaterialAlertDialog)
.setTitle(R.string.end_poll_confirmation_title)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFact
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.ReactionsSummaryFactory
import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
Expand Down Expand Up @@ -86,7 +87,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
@TimelineEventControllerHandler
private val backgroundHandler: Handler,
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper,
private val readReceiptsItemFactory: ReadReceiptsItemFactory
private val readReceiptsItemFactory: ReadReceiptsItemFactory,
private val reactionListFactory: ReactionsSummaryFactory
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {

/**
Expand Down Expand Up @@ -138,6 +140,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
fun getPreviewUrlRetriever(): PreviewUrlRetriever

fun onVoiceControlButtonClicked(eventId: String, messageAudioContent: MessageAudioContent)

fun onAddMoreReaction(event: TimelineEvent)
}

interface ReactionPillCallback {
Expand Down Expand Up @@ -283,13 +287,15 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
super.onAttachedToRecyclerView(recyclerView)
timeline?.addListener(this)
timelineMediaSizeProvider.recyclerView = recyclerView
reactionListFactory.onRequestBuild = { requestModelBuild() }
}

override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
timelineMediaSizeProvider.recyclerView = null
contentUploadStateTrackerBinder.clear()
contentDownloadStateTrackerBinder.clear()
timeline?.removeListener(this)
reactionListFactory.onRequestBuild = null
super.onDetachedFromRecyclerView(recyclerView)
}

Expand Down Expand Up @@ -383,7 +389,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val event = currentSnapshot[position]
val nextEvent = currentSnapshot.nextOrNull(position)
// Should be build if not cached or if model should be refreshed
if (modelCache[position] == null || modelCache[position]?.isCacheable(partialState) == false) {
if (modelCache[position] == null || modelCache[position]?.isCacheable(partialState) == false || reactionListFactory.needsRebuild(event)) {
val prevEvent = currentSnapshot.prevOrNull(position)
val prevDisplayableEvent = currentSnapshot.subList(0, position).lastOrNull {
timelineEventVisibilityHelper.shouldShowEvent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.PollResponseData
import im.vector.app.features.home.room.detail.timeline.item.PollVoteSummaryData
import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData
import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayoutFactory
Expand All @@ -50,7 +49,8 @@ import javax.inject.Inject
*/
class MessageInformationDataFactory @Inject constructor(private val session: Session,
private val dateFormatter: VectorDateFormatter,
private val messageLayoutFactory: TimelineMessageLayoutFactory) {
private val messageLayoutFactory: TimelineMessageLayoutFactory,
private val reactionsSummaryFactory: ReactionsSummaryFactory) {

fun create(params: TimelineItemFactoryParams): MessageInformationData {
val event = params.event
Expand Down Expand Up @@ -93,11 +93,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
avatarUrl = event.senderInfo.avatarUrl,
memberName = event.senderInfo.disambiguatedDisplayName,
messageLayout = messageLayout,
orderedReactionList = event.annotations?.reactionsSummary
// ?.filter { isSingleEmoji(it.key) }
?.map {
ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
},
reactionsSummary = reactionsSummaryFactory.create(event, params.callback),
pollResponseAggregatedSummary = event.annotations?.pollResponseSummary?.let {
PollResponseData(
myVote = it.aggregatedContent?.myVote,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.app.features.home.room.detail.timeline.helper

import dagger.hilt.android.scopes.ActivityScoped
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData
import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryData
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject

@ActivityScoped
class ReactionsSummaryFactory @Inject constructor() {

var onRequestBuild: (() -> Unit)? = null
private val showAllReactionsByEvent = HashSet<String>()
private val eventsRequestingBuild = HashSet<String>()

fun needsRebuild(event: TimelineEvent): Boolean {
return eventsRequestingBuild.remove(event.eventId)
}

fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): ReactionsSummaryData {
val eventId = event.eventId
val showAllStates = showAllReactionsByEvent.contains(eventId)
val reactions = event.annotations?.reactionsSummary
?.map {
ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
}
return ReactionsSummaryData(
reactions = reactions,
showAll = showAllStates,
onShowMoreClicked = {
showAllReactionsByEvent.add(eventId)
onRequestBuild(eventId)
},
onShowLessClicked = {
showAllReactionsByEvent.remove(eventId)
onRequestBuild(eventId)
},
onAddMoreClicked = {
callback?.onAddMoreReaction(event)
}
)
}

private fun onRequestBuild(eventId: String) {
eventsRequestingBuild.add(eventId)
onRequestBuild?.invoke()
}
}
Loading