diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt index c43ff43819..3f7d27e336 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt @@ -20,6 +20,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.internal.session.room.EventRelationExtractor import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory @@ -58,8 +59,16 @@ internal class TimelineTest : InstrumentedTest { val paginationTask = FakePaginationTask(tokenChunkEventPersistor) val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor) val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID) - val timelineEventFactory = TimelineEventFactory(roomMemberExtractor) - return DefaultTimeline(ROOM_ID, initialEventId, monarchy.realmConfiguration, taskExecutor, getContextOfEventTask, timelineEventFactory, paginationTask, null) + val timelineEventFactory = TimelineEventFactory(roomMemberExtractor, EventRelationExtractor()) + return DefaultTimeline( + ROOM_ID, + initialEventId, + monarchy.realmConfiguration, + taskExecutor, + getContextOfEventTask, + timelineEventFactory, + paginationTask, + null) } @Test diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/AggregatedAnnotation.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/AggregatedAnnotation.kt new file mode 100644 index 0000000000..3f1c619906 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/AggregatedAnnotation.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 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.matrix.android.api.session.events.model + +import com.squareup.moshi.JsonClass + +/** + * + * { + * "chunk": [ + * { + * "type": "m.reaction", + * "key": "👍", + * "count": 3 + * } + * ], + * "limited": false, + * "count": 1 + * }, + * + */ + +@JsonClass(generateAdapter = true) +data class AggregatedAnnotation ( + override val limited: Boolean? = false, + override val count: Int? = 0, + val chunk: List? = null + +) : UnsignedRelationInfo \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/AggregatedRelations.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/AggregatedRelations.kt new file mode 100644 index 0000000000..0f8d21f532 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/AggregatedRelations.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 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.matrix.android.api.session.events.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * + * { + * "m.annotation": { + * "chunk": [ + * { + * "type": "m.reaction", + * "key": "👍", + * "count": 3 + * } + * ], + * "limited": false, + * "count": 1 + * }, + * "m.reference": { + * "chunk": [ + * { + * "type": "m.room.message", + * "event_id": "$some_event_id" + * } + * ], + * "limited": false, + * "count": 1 + * } + * } + * + */ + +@JsonClass(generateAdapter = true) +data class AggregatedRelations( + @Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null, + @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/DefaultUnsignedRelationInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/DefaultUnsignedRelationInfo.kt new file mode 100644 index 0000000000..3e2df0aaf2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/DefaultUnsignedRelationInfo.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 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.matrix.android.api.session.events.model + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class DefaultUnsignedRelationInfo( + override val limited: Boolean? = false, + override val count: Int? = 0, + val chunk: List>? = null + +) : UnsignedRelationInfo \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt index 4315fd59fa..f2b67ec7e9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt @@ -65,6 +65,11 @@ object EventType { const val CALL_ANSWER = "m.call.answer" const val CALL_HANGUP = "m.call.hangup" + // Relation Events + + const val REACTION = "m.reaction" + + private val STATE_EVENTS = listOf( STATE_ROOM_NAME, STATE_ROOM_TOPIC, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/RelationChunkInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/RelationChunkInfo.kt new file mode 100644 index 0000000000..f4f1d8667d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/RelationChunkInfo.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 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.matrix.android.api.session.events.model + +import com.squareup.moshi.JsonClass + +/** + * + * { + * "type": "m.reaction", + * "key": "👍", + * "count": 3 + * } + * + */ + +@JsonClass(generateAdapter = true) +data class RelationChunkInfo( + val type: String, + val key: String, + val count: Int +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/RelationType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/RelationType.kt new file mode 100644 index 0000000000..56d4801c45 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/RelationType.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 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.matrix.android.api.session.events.model + + +/** + * Constants defining known event relation types from Matrix specifications. + */ +object RelationType { + + /** Lets you define an event which annotates an existing event.*/ + const val ANNOTATION = "m.annotation" + /** Lets you define an event which replaces an existing event.*/ + const val REPLACE = "m.replace" + /** ets you define an event which references an existing event.*/ + const val REFERENCE = "m.reference" + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt index 6a2ea226f1..004495b57d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt @@ -24,5 +24,6 @@ data class UnsignedData( @Json(name = "age") val age: Long?, @Json(name = "redacted_because") val redactedEvent: Event? = null, @Json(name = "transaction_id") val transactionId: String? = null, - @Json(name = "prev_content") val prevContent: Map? = null + @Json(name = "prev_content") val prevContent: Map? = null, + @Json(name = "m.relations") val relations: AggregatedRelations? ) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedRelationInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedRelationInfo.kt new file mode 100644 index 0000000000..5b627b6236 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedRelationInfo.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2019 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.matrix.android.api.session.events.model + + +interface UnsignedRelationInfo { + val limited : Boolean? + val count: Int? +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EventAnnotationsSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EventAnnotationsSummary.kt new file mode 100644 index 0000000000..1500843917 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EventAnnotationsSummary.kt @@ -0,0 +1,7 @@ +package im.vector.matrix.android.api.session.room.model + + +data class EventAnnotationsSummary( + var eventId: String, + var reactionsSummary: List +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReactionAggregatedSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReactionAggregatedSummary.kt new file mode 100644 index 0000000000..a6948cd1ea --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReactionAggregatedSummary.kt @@ -0,0 +1,9 @@ +package im.vector.matrix.android.api.session.room.model + +data class ReactionAggregatedSummary( + val key: String, // "👍" + val count: Int, // 8 + val addedByMe: Boolean, // true + val firstTimestamp: Long, // unix timestamp + val sourceEvents: List +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionContent.kt new file mode 100644 index 0000000000..02d4164dc4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionContent.kt @@ -0,0 +1,9 @@ +package im.vector.matrix.android.api.session.room.model.annotation + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ReactionContent( + @Json(name = "m.relates_to") val relatesTo: ReactionInfo? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionInfo.kt new file mode 100644 index 0000000000..1444450813 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionInfo.kt @@ -0,0 +1,11 @@ +package im.vector.matrix.android.api.session.room.model.annotation + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ReactionInfo( + @Json(name = "rel_type") override val type: String, + @Json(name = "event_id") override val eventId: String, + val key: String +) : RelationContent \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationContent.kt new file mode 100644 index 0000000000..ccdd10a062 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationContent.kt @@ -0,0 +1,6 @@ +package im.vector.matrix.android.api.session.room.model.annotation + +interface RelationContent { + val type: String + val eventId: String +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationDefaultContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationDefaultContent.kt new file mode 100644 index 0000000000..1d00b1a482 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationDefaultContent.kt @@ -0,0 +1,8 @@ +package im.vector.matrix.android.api.session.room.model.annotation + +import com.squareup.moshi.Json + +data class RelationDefaultContent( + @Json(name = "rel_type") override val type: String, + @Json(name = "event_id") override val eventId: String +) : RelationContent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index 7c73d761af..4e583346b0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room.timeline import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.send.SendState @@ -32,7 +33,8 @@ data class TimelineEvent( val displayIndex: Int, val senderName: String?, val senderAvatar: String?, - val sendState: SendState + val sendState: SendState, + val annotations: EventAnnotationsSummary? = null ) { val metadata = HashMap() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt new file mode 100644 index 0000000000..38388950cd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt @@ -0,0 +1,26 @@ +package im.vector.matrix.android.internal.database.mapper + +import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary +import im.vector.matrix.android.api.session.room.model.ReactionAggregatedSummary +import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity + +internal object EventAnnotationsSummaryMapper { + fun map(annotationsSummary: EventAnnotationsSummaryEntity): EventAnnotationsSummary { + return EventAnnotationsSummary( + eventId = annotationsSummary.eventId, + reactionsSummary = annotationsSummary.reactionsSummary.toList().map { + ReactionAggregatedSummary( + it.key, + it.count, + it.addedByMe, + it.firstTimestamp, + it.sourceEvents.toList() + ) + } + ) + } +} + +internal fun EventAnnotationsSummaryEntity.asDomain(): EventAnnotationsSummary { + return EventAnnotationsSummaryMapper.map(this) +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt index 3d83881a55..0d6405f825 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt @@ -19,12 +19,15 @@ package im.vector.matrix.android.internal.database.mapper import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.UnsignedData import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.di.MoshiProvider internal object EventMapper { fun map(event: Event, roomId: String): EventEntity { + val uds = if (event.unsignedData == null) null + else MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(event.unsignedData) val eventEntity = EventEntity() eventEntity.eventId = event.eventId ?: "" eventEntity.roomId = event.roomId ?: roomId @@ -37,10 +40,14 @@ internal object EventMapper { eventEntity.originServerTs = event.originServerTs eventEntity.redacts = event.redacts eventEntity.age = event.unsignedData?.age ?: event.originServerTs + eventEntity.unsignedData = uds return eventEntity } fun map(eventEntity: EventEntity): Event { + //TODO proxy the event to only parse unsigned data when accessed? + var ud = if (eventEntity.unsignedData.isNullOrBlank()) null + else MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).fromJson(eventEntity.unsignedData) return Event( type = eventEntity.type, eventId = eventEntity.eventId, @@ -50,7 +57,7 @@ internal object EventMapper { sender = eventEntity.sender, stateKey = eventEntity.stateKey, roomId = eventEntity.roomId, - unsignedData = UnsignedData(eventEntity.age), + unsignedData = ud, redacts = eventEntity.redacts ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventAnnotationsSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventAnnotationsSummaryEntity.kt new file mode 100644 index 0000000000..bd4c207746 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventAnnotationsSummaryEntity.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 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.matrix.android.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + + +internal open class EventAnnotationsSummaryEntity( + @PrimaryKey + var eventId: String = "", + var roomId: String? = null, + var reactionsSummary: RealmList = RealmList() +) : RealmObject() { + + companion object + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt index c2fa36c17b..38f4f4518c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt @@ -36,6 +36,7 @@ internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUI var originServerTs: Long? = null, @Index var sender: String? = null, var age: Long? = 0, + var unsignedData: String? = null, var redacts: String? = null, @Index var stateIndex: Int = 0, @Index var displayIndex: Int = 0, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReactionAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReactionAggregatedSummaryEntity.kt new file mode 100644 index 0000000000..93ec8bd7c5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReactionAggregatedSummaryEntity.kt @@ -0,0 +1,24 @@ +package im.vector.matrix.android.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +/** + * Aggregated Summary of a reaction. + */ +internal open class ReactionAggregatedSummaryEntity( + // The reaction String 😀 + var key: String = "", + // Number of time this reaction was selected + var count: Int = 0, + // Did the current user sent this reaction + var addedByMe: Boolean = false, + // The first time this reaction was added (for ordering purpose) + var firstTimestamp: Long = 0, + // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) + var sourceEvents: RealmList = RealmList() +) : RealmObject() { + + companion object + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt index a50426348b..dc3ad001aa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt @@ -33,6 +33,8 @@ import io.realm.annotations.RealmModule RoomSummaryEntity::class, RoomTagEntity::class, SyncEntity::class, - UserEntity::class + UserEntity::class, + EventAnnotationsSummaryEntity::class, + ReactionAggregatedSummaryEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventAnnotationsSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventAnnotationsSummaryEntityQuery.kt new file mode 100644 index 0000000000..b738af3531 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventAnnotationsSummaryEntityQuery.kt @@ -0,0 +1,27 @@ +package im.vector.matrix.android.internal.database.query + +import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity +import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { + val query = realm.where() + query.equalTo(EventAnnotationsSummaryEntityFields.EVENT_ID, eventId) + return query +} + +internal fun EventAnnotationsSummaryEntity.Companion.whereInRoom(realm: Realm, roomId: String?): RealmQuery { + val query = realm.where() + if (roomId != null) { + query.equalTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId) + } + return query +} + + +internal fun EventAnnotationsSummaryEntity.Companion.create(realm: Realm, eventId: String): EventAnnotationsSummaryEntity { + val obj = realm.createObject(EventAnnotationsSummaryEntity::class.java, eventId) + return obj +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt index becc0cfe30..45eb783116 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt @@ -17,6 +17,8 @@ package im.vector.matrix.android.internal.di import com.squareup.moshi.Moshi +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.room.model.annotation.RelationContent import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageDefaultContent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index accb91ec73..9ae7351c72 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -34,6 +34,7 @@ import im.vector.matrix.android.internal.session.filter.* import im.vector.matrix.android.internal.session.group.DefaultGroupService import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.room.DefaultRoomService +import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater import im.vector.matrix.android.internal.session.room.RoomAvatarResolver import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver @@ -102,6 +103,10 @@ internal class SessionModule(private val sessionParams: SessionParams) { RoomSummaryUpdater(get(), get(), get()) } + scope(DefaultSession.SCOPE) { + EventRelationsAggregationUpdater(get()) + } + scope(DefaultSession.SCOPE) { DefaultRoomService(get(), get(), get(), get()) as RoomService } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationExtractor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationExtractor.kt new file mode 100644 index 0000000000..6fc1cb8665 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationExtractor.kt @@ -0,0 +1,16 @@ +package im.vector.matrix.android.internal.session.room + +import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary +import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.query.where +import io.realm.Realm + + +internal class EventRelationExtractor { + + fun extractFrom(event: EventEntity, realm: Realm = event.realm): EventAnnotationsSummary? { + return EventAnnotationsSummaryEntity.where(realm, event.eventId).findFirst()?.asDomain() + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt new file mode 100644 index 0000000000..0df08914d9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt @@ -0,0 +1,84 @@ +package im.vector.matrix.android.internal.session.room + +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.events.model.* +import im.vector.matrix.android.api.session.room.model.annotation.ReactionContent +import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity +import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntity +import im.vector.matrix.android.internal.database.query.create +import im.vector.matrix.android.internal.database.query.where +import io.realm.Realm +import timber.log.Timber + + +internal class EventRelationsAggregationUpdater(private val credentials: Credentials) { + + fun update(realm: Realm, roomId: String, events: List?) { + events?.forEach { event -> + when (event.type) { + EventType.REACTION -> { + //we got a reaction!! + Timber.v("###REACTION in room $roomId") + handleReaction(event, roomId, realm) + } + EventType.MESSAGE -> { + event.unsignedData?.relations?.annotations?.let { + Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}") + handleInitialAggregatedRelations(event, roomId, it, realm) + } + //TODO message edits + } + } + } + } + + private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) { + aggregation.chunk?.forEach { + if (it.type == EventType.REACTION) { + val eventId = event.eventId ?: "" + val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + if (existing == null) { + val eventSummary = EventAnnotationsSummaryEntity.create(realm, eventId) + eventSummary.roomId = roomId + val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) + sum.key = it.key + sum.firstTimestamp = event.originServerTs ?: 0 //TODO how to maintain order? + sum.count = it.count + eventSummary.reactionsSummary.add(sum) + } else { + //TODO how to handle that + } + } + } + } + + private fun handleReaction(event: Event, roomId: String, realm: Realm) { + event.content.toModel()?.let { content -> + //rel_type must be m.annotation + if (RelationType.ANNOTATION == content.relatesTo?.type) { + val reaction = content.relatesTo.key + val eventId = content.relatesTo.eventId + val eventSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + ?: EventAnnotationsSummaryEntity.create(realm, eventId).apply { this.roomId = roomId } + + var sum = eventSummary.reactionsSummary.find { it.key == reaction } + if (sum == null) { + sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) + sum.key = reaction + sum.firstTimestamp = event.originServerTs ?: 0 + sum.count = 1 + sum.addedByMe = sum.addedByMe || (credentials.userId == event.sender) + eventSummary.reactionsSummary.add(sum) + } else { + //is this a known event (is possible? pagination?) + if (!sum.sourceEvents.contains(eventId)) { + sum.count += 1 + sum.sourceEvents.add(eventId) + sum.addedByMe = sum.addedByMe || (credentials.userId == event.sender) + } + } + + } + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 24e513551e..7bf9d96413 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -46,7 +46,7 @@ internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask, fun instantiate(roomId: String): Room { val roomMemberExtractor = SenderRoomMemberExtractor(roomId) - val timelineEventFactory = TimelineEventFactory(roomMemberExtractor) + val timelineEventFactory = TimelineEventFactory(roomMemberExtractor, EventRelationExtractor()) val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask) val sendService = DefaultSendService(roomId, eventFactory, monarchy) val stateService = DefaultStateService(roomId, sendStateTask, taskExecutor) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 973b738130..07e8c6c1ed 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -51,7 +51,7 @@ class RoomModule { } scope(DefaultSession.SCOPE) { - TokenChunkEventPersistor(get()) + TokenChunkEventPersistor(get(), get()) } scope(DefaultSession.SCOPE) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 58274288e3..f7297f273a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -27,29 +27,22 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.api.util.addTo -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.ChunkEntityFields -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.EventEntityFields -import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.query.findIncludingEvent 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.whereInRoom import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.Debouncer -import io.realm.OrderedCollectionChangeSet -import io.realm.OrderedRealmCollectionChangeListener -import io.realm.Realm -import io.realm.RealmConfiguration -import io.realm.RealmQuery -import io.realm.RealmResults -import io.realm.Sort +import io.realm.* import timber.log.Timber import java.util.* import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference import kotlin.collections.ArrayList +import kotlin.collections.HashMap private const val INITIAL_LOAD_SIZE = 20 @@ -92,19 +85,23 @@ internal class DefaultTimeline( private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN private val isLive = initialEventId == null private val builtEvents = Collections.synchronizedList(ArrayList()) + private val builtEventsIdMap = Collections.synchronizedMap(HashMap()) private val backwardsPaginationState = AtomicReference(PaginationState()) private val forwardsPaginationState = AtomicReference(PaginationState()) + private lateinit var eventRelations: RealmResults + private val eventsChangeListener = OrderedRealmCollectionChangeListener> { _, changeSet -> - if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL ) { + if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) { handleInitialLoad() } else { // If changeSet has deletion we are having a gap, so we clear everything - if(changeSet.deletionRanges.isNotEmpty()){ + if (changeSet.deletionRanges.isNotEmpty()) { prevDisplayIndex = DISPLAY_INDEX_UNKNOWN nextDisplayIndex = DISPLAY_INDEX_UNKNOWN builtEvents.clear() + builtEventsIdMap.clear() timelineEventFactory.clear() } changeSet.insertionRanges.forEach { range -> @@ -130,6 +127,38 @@ internal class DefaultTimeline( } } + private val relationsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> + + var hasChange = false + + (changeSet.insertions + changeSet.changes).forEach { + val eventRelations = collection[it] + if (eventRelations != null) { + builtEventsIdMap[eventRelations.eventId]?.let { builtIndex -> + //Update the relation of existing event + builtEvents[builtIndex]?.let { te -> + builtEvents[builtIndex] = te.copy(annotations = eventRelations.asDomain()) + hasChange = true + } + } + } + } + changeSet.deletions?.forEach { + val eventRelations = collection[it] + if (eventRelations != null) { + builtEventsIdMap[eventRelations.eventId]?.let { builtIndex -> + //Update the relation of existing event + builtEvents[builtIndex]?.let { te -> + builtEvents[builtIndex] = te.copy(annotations = null) + hasChange = true + } + } + } + } + if (hasChange) + postSnapshot() + } + // Public methods ****************************************************************************** override fun paginate(direction: Timeline.Direction, count: Int) { @@ -146,6 +175,7 @@ internal class DefaultTimeline( } } + override fun start() { if (isStarted.compareAndSet(false, true)) { Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId") @@ -171,6 +201,10 @@ internal class DefaultTimeline( .also { it.addChangeListener(eventsChangeListener) } isReady.set(true) + + eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId) + .findAllAsync() + .also { it.addChangeListener(relationsListener) } } } } @@ -242,6 +276,7 @@ internal class DefaultTimeline( } else { updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) } } + return !shouldFetchMore } @@ -266,14 +301,14 @@ internal class DefaultTimeline( private fun getPaginationState(direction: Timeline.Direction): PaginationState { return when (direction) { - Timeline.Direction.FORWARDS -> forwardsPaginationState.get() + Timeline.Direction.FORWARDS -> forwardsPaginationState.get() Timeline.Direction.BACKWARDS -> backwardsPaginationState.get() } } private fun updatePaginationState(direction: Timeline.Direction, update: (PaginationState) -> PaginationState) { val stateReference = when (direction) { - Timeline.Direction.FORWARDS -> forwardsPaginationState + Timeline.Direction.FORWARDS -> forwardsPaginationState Timeline.Direction.BACKWARDS -> backwardsPaginationState } val currentValue = stateReference.get() @@ -316,9 +351,9 @@ internal class DefaultTimeline( private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { val token = getTokenLive(direction) ?: return val params = PaginationTask.Params(roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit) + from = token, + direction = direction.toPaginationDirection(), + limit = limit) Timber.v("Should fetch $limit items $direction") paginationTask.configureWith(params) @@ -384,6 +419,7 @@ internal class DefaultTimeline( val timelineEvent = timelineEventFactory.create(eventEntity) val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size builtEvents.add(position, timelineEvent) + builtEventsIdMap[eventEntity.eventId] = position } Timber.v("Built ${offsetResults.size} items from db") return offsetResults.size diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt index 7fe3839185..8fd15c188d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt @@ -19,10 +19,13 @@ package im.vector.matrix.android.internal.session.room.timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.session.room.EventRelationExtractor import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor import io.realm.Realm -internal class TimelineEventFactory(private val roomMemberExtractor: SenderRoomMemberExtractor) { +internal class TimelineEventFactory( + private val roomMemberExtractor: SenderRoomMemberExtractor, + private val relationExtractor: EventRelationExtractor) { private val cached = mutableMapOf() @@ -30,20 +33,22 @@ internal class TimelineEventFactory(private val roomMemberExtractor: SenderRoomM val sender = eventEntity.sender val cacheKey = sender + eventEntity.stateIndex val senderData = cached.getOrPut(cacheKey) { - val senderRoomMember = roomMemberExtractor.extractFrom(eventEntity,realm) + val senderRoomMember = roomMemberExtractor.extractFrom(eventEntity, realm) SenderData(senderRoomMember?.displayName, senderRoomMember?.avatarUrl) } + val relations = relationExtractor.extractFrom(eventEntity, realm) return TimelineEvent( eventEntity.asDomain(), eventEntity.localId, eventEntity.displayIndex, senderData.senderName, senderData.senderAvatar, - eventEntity.sendState + eventEntity.sendState, + relations ) } - fun clear(){ + fun clear() { cached.clear() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index 283b807c7e..ec85c558ac 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -24,12 +24,14 @@ import im.vector.matrix.android.internal.database.helper.addStateEvents import im.vector.matrix.android.internal.database.helper.deleteOnCascade import im.vector.matrix.android.internal.database.helper.isUnlinked import im.vector.matrix.android.internal.database.helper.merge +import im.vector.matrix.android.internal.database.mapper.EventMapper import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.create import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findAllIncludingEvents import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater import im.vector.matrix.android.internal.util.tryTransactionSync import io.realm.kotlin.createObject import timber.log.Timber @@ -37,7 +39,8 @@ import timber.log.Timber /** * Insert Chunk in DB, and eventually merge with existing chunk event */ -internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { +internal class TokenChunkEventPersistor(private val monarchy: Monarchy, + private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) { /** *
@@ -147,6 +150,9 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
                     } else {
                         Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
                         currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
+
+                        //Event
+                        eventRelationsAggregationUpdater.update(realm,roomId,receivedChunk.events.toList())
                         // Then we merge chunks if needed
                         if (currentChunk != prevChunk && prevChunk != null) {
                             currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
index 24dff3398a..d5ec714c7b 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
@@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.database.model.RoomEntity
 import im.vector.matrix.android.internal.database.query.find
 import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
 import im.vector.matrix.android.internal.database.query.where
+import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
 import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
 import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
 import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
@@ -45,7 +46,8 @@ import timber.log.Timber
 internal class RoomSyncHandler(private val monarchy: Monarchy,
                                private val readReceiptHandler: ReadReceiptHandler,
                                private val roomSummaryUpdater: RoomSummaryUpdater,
-                               private val roomTagHandler: RoomTagHandler) {
+                               private val roomTagHandler: RoomTagHandler,
+                               private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) {
 
     sealed class HandlingStrategy {
         data class JOINED(val data: Map) : HandlingStrategy()
@@ -120,6 +122,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
             }
         }
         roomSummaryUpdater.update(realm, roomId, roomSync.summary, roomSync.unreadNotifications)
+        eventRelationsAggregationUpdater.update(realm,roomId,roomSync.timeline?.events)
 
         if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
             handleEphemeral(realm, roomId, roomSync.ephemeral)
@@ -174,6 +177,9 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
         lastChunk?.isLastForward = false
         chunkEntity.isLastForward = true
         chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS, stateIndexOffset)
+
+        //update eventAnnotationSummary here?
+
         return chunkEntity
     }
 
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt
index d0f6040520..d601406116 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt
@@ -40,7 +40,7 @@ internal class SyncModule {
         }
 
         scope(DefaultSession.SCOPE) {
-            RoomSyncHandler(get(), get(), get(), get())
+            RoomSyncHandler(get(), get(), get(), get(), get())
         }
 
         scope(DefaultSession.SCOPE) {
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 60d1aebbbb..507bf4cd93 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
@@ -59,6 +59,7 @@ import im.vector.matrix.android.api.session.user.model.User
 import im.vector.riotredesign.R
 import im.vector.riotredesign.core.dialogs.DialogListItem
 import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
+import im.vector.riotredesign.core.extensions.hideKeyboard
 import im.vector.riotredesign.core.extensions.observeEvent
 import im.vector.riotredesign.core.glide.GlideApp
 import im.vector.riotredesign.core.platform.ToolbarConfigurable
@@ -460,6 +461,7 @@ class RoomDetailFragment :
             Timber.e("Missing RoomId, cannot open bottomsheet")
             return false
         }
+        this.view?.hideKeyboard()
         MessageActionsBottomSheet
                 .newInstance(roomId, informationData)
                 .show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS")
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 320332ba43..180bacc09f 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
@@ -89,7 +89,7 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() {
 
         var quickReactionFragment = cfm.findFragmentByTag("QuickReaction") as? QuickReactionFragment
         if (quickReactionFragment == null) {
-            quickReactionFragment = QuickReactionFragment.newInstance()
+            quickReactionFragment = QuickReactionFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as ParcelableArgs)
             cfm.beginTransaction()
                     .replace(R.id.bottom_sheet_quick_reaction_container, quickReactionFragment, "QuickReaction")
                     .commit()
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 a8593a2e97..70bb18c6c6 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
@@ -25,6 +25,7 @@ import androidx.transition.TransitionManager
 import butterknife.BindView
 import butterknife.ButterKnife
 import com.airbnb.mvrx.BaseMvRxFragment
+import com.airbnb.mvrx.MvRx
 import com.airbnb.mvrx.fragmentViewModel
 import com.airbnb.mvrx.withState
 import im.vector.riotredesign.R
@@ -62,10 +63,10 @@ class QuickReactionFragment : BaseMvRxFragment() {
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
-        quickReact1Text.text = viewModel.agreePositive
-        quickReact2Text.text = viewModel.agreeNegative
-        quickReact3Text.text = viewModel.likePositive
-        quickReact4Text.text = viewModel.likeNegative
+        quickReact1Text.text = QuickReactionViewModel.agreePositive
+        quickReact2Text.text = QuickReactionViewModel.agreeNegative
+        quickReact3Text.text = QuickReactionViewModel.likePositive
+        quickReact4Text.text = QuickReactionViewModel.likeNegative
 
         //configure click listeners
         quickReact1Text.setOnClickListener {
@@ -127,8 +128,12 @@ class QuickReactionFragment : BaseMvRxFragment() {
     }
 
     companion object {
-        fun newInstance(): QuickReactionFragment {
-            return QuickReactionFragment()
+        fun newInstance(pa: MessageActionsBottomSheet.ParcelableArgs): QuickReactionFragment {
+            val args = Bundle()
+            args.putParcelable(MvRx.KEY_ARG, pa)
+            val fragment = QuickReactionFragment()
+            fragment.arguments = args
+            return fragment
         }
     }
 }
\ No newline at end of file
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 a4b9834444..2bc1a8dd55 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
@@ -18,7 +18,9 @@ package im.vector.riotredesign.features.home.room.detail.timeline.action
 import com.airbnb.mvrx.MvRxState
 import com.airbnb.mvrx.MvRxViewModelFactory
 import com.airbnb.mvrx.ViewModelContext
+import im.vector.matrix.android.api.session.Session
 import im.vector.riotredesign.core.platform.VectorViewModel
+import org.koin.android.ext.android.get
 
 /**
  * Quick reactions state, it's a toggle with 3rd state
@@ -37,11 +39,6 @@ data class QuickReactionState(val agreeTrigleState: TriggleState, val likeTriggl
  */
 class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel(initialState) {
 
-    val agreePositive = "👍"
-    val agreeNegative = "👎"
-    val likePositive = "😀"
-    val likeNegative = "😞"
-
 
     fun toggleAgree(isFirst: Boolean) = withState {
         if (isFirst) {
@@ -99,10 +96,37 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel
 
     companion object : MvRxViewModelFactory {
 
+        val agreePositive = "👍"
+        val agreeNegative = "👎"
+        val likePositive = "🙂"
+        val likeNegative = "😔"
+
         override fun initialState(viewModelContext: ViewModelContext): QuickReactionState? {
             // Args are accessible from the context.
             // val foo = vieWModelContext.args.foo
-            return QuickReactionState(TriggleState.NONE, TriggleState.NONE)
+            val currentSession = viewModelContext.activity.get()
+            val parcel = viewModelContext.args as MessageActionsBottomSheet.ParcelableArgs
+            val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId)
+                    ?: return null
+            var agreeTriggle: TriggleState = TriggleState.NONE
+            var likeTriggle: TriggleState = TriggleState.NONE
+            event.annotations?.reactionsSummary?.forEach {
+                //it.addedByMe
+                if (it.addedByMe) {
+                    if (agreePositive == it.key) {
+                        agreeTriggle = TriggleState.FIRST
+                    } else if (agreeNegative == it.key) {
+                        agreeTriggle = TriggleState.SECOND
+                    }
+
+                    if (likePositive == it.key) {
+                        likeTriggle = TriggleState.FIRST
+                    } else if (likeNegative == it.key) {
+                        likeTriggle = TriggleState.SECOND
+                    }
+                }
+            }
+            return QuickReactionState(agreeTriggle, likeTriggle)
         }
     }
 }
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index f8ba03de0e..8afdf200a8 100644
--- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -71,7 +71,8 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
         val avatarUrl = event.senderAvatar
         val memberName = event.senderName ?: event.root.sender ?: ""
         val formattedMemberName = span(memberName) {
-            textColor = colorProvider.getColor(AvatarRenderer.getColorFromUserId(event.root.sender ?: ""))
+            textColor = colorProvider.getColor(AvatarRenderer.getColorFromUserId(event.root.sender
+                    ?: ""))
         }
         val informationData = MessageInformationData(eventId = eventId,
                 senderId = event.root.sender ?: "",
@@ -79,7 +80,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
                 time = time,
                 avatarUrl = avatarUrl,
                 memberName = formattedMemberName,
-                showInformation = showInformation)
+                showInformation = showInformation,
+                orderedReactionList = event.annotations?.reactionsSummary?.map { Triple(it.key, it.count, it.addedByMe) }
+        )
 
         //Test for reactions UX
         //informationData.orderedReactionList = listOf( Triple("👍",1,false), Triple("👎",2,false))
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 94492e3638..045bb1c76d 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
@@ -19,6 +19,7 @@ package im.vector.riotredesign.features.home.room.detail.timeline.item
 import android.os.Build
 import android.view.View
 import android.view.ViewGroup
+import android.view.ViewStub
 import android.widget.ImageView
 import android.widget.TextView
 import androidx.constraintlayout.helper.widget.Flow
@@ -76,14 +77,19 @@ abstract class AbsMessageItem : BaseEventItem() {
         holder.view.setOnLongClickListener(longClickListener)
 
         if (informationData.orderedReactionList.isNullOrEmpty()) {
-            holder.reactionWrapper.isVisible = false
+            holder.reactionWrapper?.isVisible = false
         } else {
-            holder.reactionWrapper.isVisible = true
+            //inflate if needed
+            if (holder.reactionFlowHelper == null) {
+                holder.reactionWrapper = holder.view.findViewById(R.id.messageBottomInfo).inflate() as? ViewGroup
+                holder.reactionFlowHelper = holder.view.findViewById(R.id.reactionsFlowHelper)
+            }
+            holder.reactionWrapper?.isVisible = true
             //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()
             informationData.orderedReactionList?.forEachIndexed { index, reaction ->
-                (holder.reactionWrapper.children.elementAt(index) as? ReactionButton)?.let { reactionButton ->
+                (holder.reactionWrapper?.children?.elementAt(index) as? ReactionButton)?.let { reactionButton ->
                     reactionButton.isVisible = true
                     idToRefInFlow.add(reactionButton.id)
                     reactionButton.reactionString = reaction.first
@@ -93,9 +99,9 @@ abstract class AbsMessageItem : BaseEventItem() {
             }
             // Just setting the view as gone will break the FlowHelper (and invisible will take too much space),
             // so have to update ref ids
-            holder.reactionFlowHelper.referencedIds = idToRefInFlow.toIntArray()
+            holder.reactionFlowHelper?.referencedIds = idToRefInFlow.toIntArray()
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && !holder.view.isInLayout) {
-                holder.reactionFlowHelper.requestLayout()
+                holder.reactionFlowHelper?.requestLayout()
             }
 
         }
@@ -112,8 +118,8 @@ abstract class AbsMessageItem : BaseEventItem() {
         val memberNameView by bind(R.id.messageMemberNameView)
         val timeView by bind(R.id.messageTimeView)
 
-        val reactionWrapper: ViewGroup by bind(R.id.messageBottomInfo)
-        val reactionFlowHelper: Flow by bind(R.id.reactionsFlowHelper)
+        var reactionWrapper: ViewGroup? = null
+        var reactionFlowHelper: Flow? = null
     }
 
 }
\ No newline at end of file
diff --git a/vector/src/main/res/drawable/rounded_rect_shape.xml b/vector/src/main/res/drawable/rounded_rect_shape.xml
index dbb12374ae..cf083254f3 100644
--- a/vector/src/main/res/drawable/rounded_rect_shape.xml
+++ b/vector/src/main/res/drawable/rounded_rect_shape.xml
@@ -4,7 +4,7 @@
 
     
 
-    
+    
 
     
 
diff --git a/vector/src/main/res/drawable/rounded_rect_shape_off.xml b/vector/src/main/res/drawable/rounded_rect_shape_off.xml
index e72c738356..8dac3020e3 100644
--- a/vector/src/main/res/drawable/rounded_rect_shape_off.xml
+++ b/vector/src/main/res/drawable/rounded_rect_shape_off.xml
@@ -4,9 +4,9 @@
 
     
 
-    
+    
 
-    
+    
 
     
 
diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml
index 89c3b31871..dc2e2ed67e 100644
--- a/vector/src/main/res/layout/item_timeline_event_base.xml
+++ b/vector/src/main/res/layout/item_timeline_event_base.xml
@@ -82,116 +82,23 @@
         tools:ignore="MissingConstraints" />
 
 
+
     
-    
 
-        
-
-
-        
-
-
-        
-
-
-        
-
-
-        
-
-
-        
-
-
-        
-
-        
-
-
-        
-
-    
+    
 
 
 
\ No newline at end of file
diff --git a/vector/src/main/res/layout/item_timeline_event_bottom_reactions_stub.xml b/vector/src/main/res/layout/item_timeline_event_bottom_reactions_stub.xml
new file mode 100644
index 0000000000..13ca78ae48
--- /dev/null
+++ b/vector/src/main/res/layout/item_timeline_event_bottom_reactions_stub.xml
@@ -0,0 +1,102 @@
+
+
+
+    
+
+    
+
+    
+
+    
+
+    
+
+
+    
+
+
+    
+
+    
+
+
+    
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml
index 7ab2caa12a..530108f397 100644
--- a/vector/src/main/res/values/styles_riot.xml
+++ b/vector/src/main/res/values/styles_riot.xml
@@ -252,7 +252,7 @@
         wrap_content
         8dp
         8dp
-        8dp
+        4dp
         4dp
         parent
         parent