Skip to content

Commit

Permalink
Render image reactions (MSC3746)
Browse files Browse the repository at this point in the history
Some notes:
- Doesn't re-parse reactions already in the db to add the url field - so
  may need an initial sync for those.
- Since some clients don't really follow MSC3746, as in: they don't use
  the url field, but instead only write and check the key if it is an
  mxc-url, support those as well.
- Accordingly, initial sync is likely not required for those reactions
  I've seen in the wild so far, as it's common to use the mxc url also
  as key.

Change-Id: Ib1c50315425494986fa2e794d165658220a4f342
  • Loading branch information
SpiritCroc committed May 11, 2022
1 parent 8855665 commit 85a26ae
Show file tree
Hide file tree
Showing 19 changed files with 160 additions and 7 deletions.
1 change: 1 addition & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Here you can find some extra features and changes compared to Element Android (w
- Setting to re-alert for new messages even if there's still an old notification for that room
- Setting to hide start call buttons from the room's toolbar
- Render inline images / custom emojis in the timeline
- Render image reactions

- Branding (name, app icon, links)
- Show a toast instead of a snackbar after copying text, in order to not block the input area right after copying
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package de.spiritcroc.android.sdk.internal.database.migration

import de.spiritcroc.android.sdk.internal.util.database.ScRealmMigrator
import io.realm.DynamicRealm
import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntityFields

internal class MigrateScSessionTo005(realm: DynamicRealm) : ScRealmMigrator(realm, 5) {

override fun doMigrate(realm: DynamicRealm) {
realm.schema.get("ReactionAggregatedSummaryEntity")
?.addField(ReactionAggregatedSummaryEntityFields.URL, String::class.java)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room.model

data class ReactionAggregatedSummary(
val key: String, // "👍"
val url: String?, // mxc://...
val count: Int, // 8
val addedByMe: Boolean, // true
val firstTimestamp: Long, // unix timestamp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class ReactionContent(
@Json(name = "m.relates_to") val relatesTo: ReactionInfo? = null
@Json(name = "m.relates_to") val relatesTo: ReactionInfo? = null,
@Json(name = "url") val url: String? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import de.spiritcroc.android.sdk.internal.database.migration.MigrateScSessionTo0
import de.spiritcroc.android.sdk.internal.database.migration.MigrateScSessionTo002
import de.spiritcroc.android.sdk.internal.database.migration.MigrateScSessionTo003
import de.spiritcroc.android.sdk.internal.database.migration.MigrateScSessionTo004
import de.spiritcroc.android.sdk.internal.database.migration.MigrateScSessionTo005
import io.realm.DynamicRealm
import io.realm.RealmMigration
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo001
Expand Down Expand Up @@ -64,7 +65,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
override fun hashCode() = 1000

// SC-specific DB changes on top of Element
private val scSchemaVersion = 4L
private val scSchemaVersion = 5L
private val scSchemaVersionOffset = (1L shl 12)

val schemaVersion = 27L +
Expand Down Expand Up @@ -108,5 +109,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldScVersion <= 1) MigrateScSessionTo002(realm).perform()
if (oldScVersion <= 2) MigrateScSessionTo003(realm).perform()
if (oldScVersion <= 3) MigrateScSessionTo004(realm).perform()
if (oldScVersion <= 4) MigrateScSessionTo005(realm).perform()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ internal object EventAnnotationsSummaryMapper {
reactionsSummary = annotationsSummary.reactionsSummary.toList().map {
ReactionAggregatedSummary(
it.key,
it.url,
it.count,
it.addedByMe,
it.firstTimestamp,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import io.realm.RealmObject
internal open class ReactionAggregatedSummaryEntity(
// The reaction String 😀
var key: String = "",
// mxc url
var url: String? = null,
// Number of time this reaction was selected
var count: Int = 0,
// Did the current user sent this reaction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -597,19 +597,21 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
// rel_type must be m.annotation
if (RelationType.ANNOTATION == content.relatesTo?.type) {
val reaction = content.relatesTo.key
val url = content.url
val relatedEventID = content.relatesTo.eventId
val reactionEventId = event.eventId
Timber.v("Reaction $reactionEventId relates to $relatedEventID")
val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventID)

var sum = eventSummary.reactionsSummary.find { it.key == reaction }
var sum = eventSummary.reactionsSummary.find { it.key == reaction && it.url == url }
val txId = event.unsignedData?.transactionId
if (isLocalEcho && txId.isNullOrBlank()) {
Timber.w("Received a local echo with no transaction ID")
}
if (sum == null) {
sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
sum.key = reaction
sum.url = url
sum.firstTimestamp = event.originServerTs ?: 0
if (isLocalEcho) {
Timber.v("Adding local echo reaction")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ internal class UIEchoManager(
// just add the new key
ReactionAggregatedSummary(
key = uiEchoReaction.reaction,
url = null,
count = 1,
addedByMe = true,
firstTimestamp = clock.epochMillis(),
Expand All @@ -125,6 +126,7 @@ internal class UIEchoManager(
// only update if echo is not yet there
ReactionAggregatedSummary(
key = existing.key,
url = existing.url,
count = existing.count + 1,
addedByMe = true,
firstTimestamp = existing.firstTimestamp,
Expand Down
63 changes: 63 additions & 0 deletions vector/src/main/java/im/vector/app/core/glide/GlideReactionUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package im.vector.app.core.glide

import android.graphics.drawable.Drawable
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import timber.log.Timber

@Suppress("UNUSED_PARAMETER")
fun renderReactionImage(reactionUrl: String?,
reactionKey: String?,
size: Int,
session: Session,
textView: TextView,
imageView: ImageView) {
val effectiveReactionUrl = when {
!reactionUrl.isNullOrEmpty() -> reactionUrl
reactionKey?.isMxcUrl().orFalse() -> reactionKey
else -> null
}
if (effectiveReactionUrl.isNullOrEmpty()) {
textView.isVisible = true
imageView.isVisible = false
} else {
// Not all thumbnail providers allow GIFs!
//val url = session.contentUrlResolver().resolveThumbnail(effectiveReactionUrl, size, size, ContentUrlResolver.ThumbnailMethod.SCALE)
val url = session.contentUrlResolver().resolveFullSize(effectiveReactionUrl)
if (url == null) {
textView.isVisible = true
imageView.isVisible = false
} else {
textView.isVisible = false
imageView.isVisible = true
GlideApp.with(imageView)
.load(url)
.centerCrop()
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
Timber.w("Reaction image load failed for $effectiveReactionUrl: $e")
textView.isVisible = true
imageView.isVisible = false
return false
}
override fun onResourceReady(resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean): Boolean {
return false
}
})
.into(imageView)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class ReactionsSummaryFactory @Inject constructor() {
val showAllStates = showAllReactionsByEvent.contains(eventId)
val reactions = event.annotations?.reactionsSummary
?.map {
ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
ReactionInfoData(it.key, it.url, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
}
return ReactionsSummaryData(
reactions = reactions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
reactionButton.reactedListener = reactionClickListener
reactionButton.setTag(R.id.reactionsContainer, reaction.key)
reactionButton.reactionString = reaction.key
reactionButton.reactionUrl = reaction.url
reactionButton.reactionCount = reaction.count
reactionButton.setChecked(reaction.addedByMe)
reactionButton.isEnabled = reaction.synced
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ data class ReactionsSummaryEvents(
@Parcelize
data class ReactionInfoData(
val key: String,
val url: String?,
val count: Int,
val addedByMe: Boolean,
val synced: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,19 @@

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

import android.widget.ImageView
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.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.glide.renderReactionImage
import im.vector.app.core.utils.DimensionConverter
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence

/**
Expand All @@ -36,6 +40,9 @@ abstract class ReactionInfoSimpleItem : EpoxyModelWithHolder<ReactionInfoSimpleI
@EpoxyAttribute
lateinit var reactionKey: EpoxyCharSequence

@EpoxyAttribute
var reactionUrl: String? = null

@EpoxyAttribute
lateinit var authorDisplayName: String

Expand All @@ -45,6 +52,12 @@ abstract class ReactionInfoSimpleItem : EpoxyModelWithHolder<ReactionInfoSimpleI
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var userClicked: ClickListener? = null

@EpoxyAttribute
lateinit var dimensionConverter: DimensionConverter

@EpoxyAttribute
lateinit var activeSessionHolder: ActiveSessionHolder

override fun bind(holder: Holder) {
super.bind(holder)
holder.emojiReactionView.text = reactionKey.charSequence
Expand All @@ -56,10 +69,16 @@ abstract class ReactionInfoSimpleItem : EpoxyModelWithHolder<ReactionInfoSimpleI
holder.timeStampView.isVisible = false
}
holder.view.onClick(userClicked)

activeSessionHolder.getSafeActiveSession()?.let { session ->
val size = dimensionConverter.dpToPx(16)
renderReactionImage(reactionUrl, reactionKey.charSequence.toString(), size, session, holder.emojiReactionView, holder.emojiReactionImageView)
}
}

class Holder : VectorEpoxyHolder() {
val emojiReactionView by bind<TextView>(R.id.itemSimpleReactionInfoKey)
val emojiReactionImageView by bind<ImageView>(R.id.itemSimpleReactionInfoImage)
val displayNameView by bind<TextView>(R.id.itemSimpleReactionInfoMemberName)
val timeStampView by bind<TextView>(R.id.itemSimpleReactionInfoTime)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.app.EmojiSpanify
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.core.ui.list.genericLoaderItem
import im.vector.app.core.utils.DimensionConverter
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import javax.inject.Inject

Expand All @@ -34,6 +36,8 @@ import javax.inject.Inject
*/
class ViewReactionsEpoxyController @Inject constructor(
private val stringProvider: StringProvider,
private val dimensionConverter: DimensionConverter,
private val activeSessionHolder: ActiveSessionHolder,
private val emojiSpanify: EmojiSpanify) :
TypedEpoxyController<DisplayReactionsViewState>() {

Expand All @@ -60,6 +64,9 @@ class ViewReactionsEpoxyController @Inject constructor(
id(reactionInfo.eventId)
timeStamp(reactionInfo.timestamp)
reactionKey(host.emojiSpanify.spanify(reactionInfo.reactionKey).toEpoxyCharSequence())
reactionUrl(reactionInfo.reactionUrl)
dimensionConverter(host.dimensionConverter)
activeSessionHolder(host.activeSessionHolder)
authorDisplayName(reactionInfo.authorName ?: reactionInfo.authorId)
userClicked { host.listener?.didSelectUser(reactionInfo.authorId) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ data class ReactionInfo(
val reactionKey: String,
val authorId: String,
val authorName: String? = null,
val timestamp: String? = null
val timestamp: String? = null,
val reactionUrl: String? = null
)

/**
Expand Down Expand Up @@ -95,7 +96,8 @@ class ViewReactionsViewModel @AssistedInject constructor(
reactionsSummary.key,
event.root.senderId ?: "",
event.senderInfo.disambiguatedDisplayName,
dateFormatter.format(event.root.originServerTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
dateFormatter.format(event.root.originServerTs, DateFormatKind.DEFAULT_DATE_AND_TIME),
reactionUrl = reactionsSummary.url

)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import androidx.core.content.withStyledAttributes
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.EmojiSpanify
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.glide.renderReactionImage
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.core.utils.TextUtils
import im.vector.app.databinding.ReactionButtonBinding
import javax.inject.Inject
Expand All @@ -40,6 +43,9 @@ class ReactionButton @JvmOverloads constructor(context: Context,
defStyleRes: Int = R.style.TimelineReactionView) :
LinearLayout(context, attrs, defStyleAttr, defStyleRes), View.OnClickListener, View.OnLongClickListener {

@Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var dimensionConverter: DimensionConverter

@Inject lateinit var emojiSpanify: EmojiSpanify

private val views: ReactionButtonBinding
Expand All @@ -60,6 +66,16 @@ class ReactionButton @JvmOverloads constructor(context: Context,
views.reactionText.text = emojiSpanned
}

var reactionUrl: String? = null
set(value) {
field = value

activeSessionHolder.getSafeActiveSession()?.let { session ->
val size = dimensionConverter.dpToPx(12)
renderReactionImage(reactionUrl, reactionString, size, session,views.reactionText, views.reactionImage)
}
}

private var isChecked: Boolean = false
private var onDrawable: Drawable? = null
private var offDrawable: Drawable? = null
Expand Down
12 changes: 11 additions & 1 deletion vector/src/main/res/layout/item_simple_reaction_info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@
android:paddingEnd="8dp"
tools:viewBindingIgnore="true">

<ImageView
android:id="@+id/itemSimpleReactionInfoImage"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="8dp"
android:gravity="center"
android:visibility="gone"
tools:ignore="ContentDescription" />

<TextView
android:id="@+id/itemSimpleReactionInfoKey"
style="@style/Widget.Vector.TextView.HeadlineMedium"
Expand All @@ -32,4 +42,4 @@
style="@style/BottomSheetItemTime"
tools:text="10:44" />

</LinearLayout>
</LinearLayout>
9 changes: 9 additions & 0 deletions vector/src/main/res/layout/reaction_button.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
<!--android:layout_height="match_parent"-->
<!--android:background="@drawable/rounded_rect_shape" />-->

<ImageView
android:id="@+id/reactionImage"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginHorizontal="4dp"
android:gravity="center"
android:visibility="gone"
tools:ignore="ContentDescription" />

<TextView
android:id="@+id/reactionText"
style="@style/Widget.Vector.TextView.Caption"
Expand Down

0 comments on commit 85a26ae

Please sign in to comment.