From 25f1d21bc709c9a51624cf73d168dc102145b723 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 15 Jul 2019 14:26:13 +0200 Subject: [PATCH] Edit history Get history from API cleaning Updated change log Missing copyrights Code review cleaning --- CHANGES.md | 2 +- build.gradle | 6 + .../room/model/relation/RelationService.kt | 9 + .../internal/network/NetworkConstants.kt | 7 +- .../android/internal/session/room/RoomAPI.kt | 17 +- .../internal/session/room/RoomFactory.kt | 6 +- .../internal/session/room/RoomModule.kt | 3 + .../room/relation/DefaultRelationService.kt | 11 +- .../room/relation/FetchEditHistoryTask.kt | 48 ++++++ .../room/relation/RelationsResponse.kt | 27 +++ vector/build.gradle | 2 + .../src/main/assets/open_source_licenses.html | 7 + .../vector/riotx/core/di/ScreenComponent.kt | 7 +- .../vector/riotx/core/di/ViewModelModule.kt | 11 +- .../vector/riotx/core/ui/list/GenericItem.kt | 2 +- .../riotx/core/ui/list/GenericItemHeader.kt | 42 +++++ .../riotx/core/ui/list/GenericLoaderItem.kt | 20 +++ .../home/room/detail/RoomDetailActions.kt | 1 - .../home/room/detail/RoomDetailFragment.kt | 11 +- .../home/room/detail/RoomDetailViewModel.kt | 17 -- .../action/ViewEditHistoryBottomSheet.kt | 93 +++++++++++ .../action/ViewEditHistoryEpoxyController.kt | 155 ++++++++++++++++++ .../action/ViewEditHistoryViewModel.kt | 90 ++++++++++ .../action/ViewReactionBottomSheet.kt | 16 +- .../action/ViewReactionsEpoxyController.kt | 41 ++++- ... => bottom_sheet_epoxylist_with_title.xml} | 15 +- .../main/res/layout/item_generic_header.xml | 14 ++ .../main/res/layout/item_generic_loader.xml | 6 + vector/src/main/res/values/strings_riotX.xml | 4 + 29 files changed, 613 insertions(+), 77 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FetchEditHistoryTask.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/RelationsResponse.kt create mode 100644 vector/src/main/java/im/vector/riotx/core/ui/list/GenericItemHeader.kt create mode 100644 vector/src/main/java/im/vector/riotx/core/ui/list/GenericLoaderItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt rename vector/src/main/res/layout/{bottom_sheet_display_reactions.xml => bottom_sheet_epoxylist_with_title.xml} (70%) create mode 100644 vector/src/main/res/layout/item_generic_header.xml create mode 100644 vector/src/main/res/layout/item_generic_loader.xml diff --git a/CHANGES.md b/CHANGES.md index a01b74169b..e468229200 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ Changes in RiotX 0.2.1 (2019-XX-XX) =================================================== Features: - - + - Message Editing: View edit history Improvements: - Handle click on redacted events: view source and create permalink diff --git a/build.gradle b/build.gradle index 91415088e5..4fa468ab26 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,12 @@ allprojects { maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } google() jcenter() + maven { + url 'https://repo.adobe.com/nexus/content/repositories/public/' + content { + includeGroupByRegex "diff_match_patch" + } + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt index 81d7ddd4c0..7ffbd5f1c9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt @@ -16,6 +16,8 @@ package im.vector.matrix.android.api.session.room.model.relation import androidx.lifecycle.LiveData +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.Cancelable @@ -78,6 +80,11 @@ interface RelationService { newBodyAutoMarkdown: Boolean, compatibilityBodyText: String = "* $newBodyText"): Cancelable + /** + * Get's the edit history of the given event + */ + fun fetchEditHistory(eventId: String, callback: MatrixCallback>) + /** * Reply to an event in the timeline (must be in same room) @@ -91,4 +98,6 @@ interface RelationService { autoMarkdown: Boolean = false): Cancelable? fun getEventSummaryLive(eventId: String): LiveData + + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt index d6d2d9cc6c..cbd4d0c674 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt @@ -18,7 +18,8 @@ package im.vector.matrix.android.internal.network internal object NetworkConstants { - const val URI_API_PREFIX_PATH = "_matrix/client/" - const val URI_API_PREFIX_PATH_R0 = "_matrix/client/r0/" + private const val URI_API_PREFIX_PATH = "_matrix/client" + const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/" + const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/" -} \ No newline at end of file +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index af26397046..361a935d2f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -22,10 +22,11 @@ import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse +import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody -import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol +import im.vector.matrix.android.internal.session.room.relation.RelationsResponse import im.vector.matrix.android.internal.session.room.send.SendResponse import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse @@ -195,6 +196,20 @@ internal interface RoomAPI { @Body content: Content? ): Call + + /** + * Paginate relations for event based in normal topological order + * + * @param relationType filter for this relation type + * @param eventType filter for this event type + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}") + fun getRelations(@Path("roomId") roomId: String, + @Path("eventId") eventId: String, + @Path("relationType") relationType: String, + @Path("eventType") eventType: String + ): Call + /** * Join the given room. * 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 143ef60b46..98cf872b10 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 @@ -30,8 +30,8 @@ import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRo import im.vector.matrix.android.internal.session.room.read.DefaultReadService import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask import im.vector.matrix.android.internal.session.room.relation.DefaultRelationService +import im.vector.matrix.android.internal.session.room.relation.FetchEditHistoryTask import im.vector.matrix.android.internal.session.room.relation.FindReactionEventForUndoTask -import im.vector.matrix.android.internal.session.room.relation.UpdateQuickReactionTask import im.vector.matrix.android.internal.session.room.send.DefaultSendService import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.session.room.state.DefaultStateService @@ -56,7 +56,7 @@ internal class RoomFactory @Inject constructor(private val context: Context, private val setReadMarkersTask: SetReadMarkersTask, private val cryptoService: CryptoService, private val findReactionEventForUndoTask: FindReactionEventForUndoTask, - private val updateQuickReactionTask: UpdateQuickReactionTask, + private val fetchEditHistoryTask: FetchEditHistoryTask, private val joinRoomTask: JoinRoomTask, private val leaveRoomTask: LeaveRoomTask) { @@ -67,7 +67,7 @@ internal class RoomFactory @Inject constructor(private val context: Context, val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials) val relationService = DefaultRelationService(context, - credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, monarchy, taskExecutor) + credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, fetchEditHistoryTask, monarchy, taskExecutor) return DefaultRoom( roomId, 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 09322e6a1a..942239ea12 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 @@ -142,4 +142,7 @@ internal abstract class RoomModule { @Binds abstract fun bindFileService(fileService: DefaultFileService): FileService + + @Binds + abstract fun bindFetchEditHistoryTask(editHistoryTask: DefaultFetchEditHistoryTask): FetchEditHistoryTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index 3eb1c066a8..1b487d960d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -53,6 +53,7 @@ internal class DefaultRelationService @Inject constructor(private val context: C private val eventFactory: LocalEchoEventFactory, private val cryptoService: CryptoService, private val findReactionEventForUndoTask: FindReactionEventForUndoTask, + private val fetchEditHistoryTask: FetchEditHistoryTask, private val monarchy: Monarchy, private val taskExecutor: TaskExecutor) : RelationService { @@ -131,6 +132,13 @@ internal class DefaultRelationService @Inject constructor(private val context: C } + override fun fetchEditHistory(eventId: String, callback: MatrixCallback>) { + val params = FetchEditHistoryTask.Params(roomId, eventId) + fetchEditHistoryTask.configureWith(params) + .dispatchTo(callback) + .executeBy(taskExecutor) + } + override fun replyToMessage(eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Cancelable? { val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown)?.also { saveLocalEcho(it) @@ -169,7 +177,8 @@ internal class DefaultRelationService @Inject constructor(private val context: C EventAnnotationsSummaryEntity.where(realm, eventId) } return Transformations.map(liveEntity) { realmResults -> - realmResults.firstOrNull()?.asDomain() ?: EventAnnotationsSummary(eventId, emptyList(), null) + realmResults.firstOrNull()?.asDomain() + ?: EventAnnotationsSummary(eventId, emptyList(), null) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FetchEditHistoryTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FetchEditHistoryTask.kt new file mode 100644 index 0000000000..e891ece8da --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FetchEditHistoryTask.kt @@ -0,0 +1,48 @@ +/* + * 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.session.room.relation + +import arrow.core.Try +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.events.model.RelationType +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + + +internal interface FetchEditHistoryTask : Task> { + + data class Params( + val roomId: String, + val eventId: String + ) +} + + +internal class DefaultFetchEditHistoryTask @Inject constructor( + private val roomAPI: RoomAPI +) : FetchEditHistoryTask { + + override suspend fun execute(params: FetchEditHistoryTask.Params): Try> { + return executeRequest { + apiCall = roomAPI.getRelations(params.roomId, params.eventId, RelationType.REPLACE, EventType.MESSAGE) + }.map { resp -> + resp.chunks + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/RelationsResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/RelationsResponse.kt new file mode 100644 index 0000000000..5d39e81c48 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/RelationsResponse.kt @@ -0,0 +1,27 @@ +/* + * 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.session.room.relation + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Event + +@JsonClass(generateAdapter = true) +internal data class RelationsResponse( + @Json(name = "chunk") val chunks: List, + @Json(name = "next_batch") val nextBatch: String?, + @Json(name = "prev_batch") val prevBatch: String? +) \ No newline at end of file diff --git a/vector/build.gradle b/vector/build.gradle index ace3135ae5..d7332c476c 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -254,6 +254,8 @@ dependencies { exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' } + implementation 'diff_match_patch:diff_match_patch:current' + // TESTS testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index cec7c7183d..9e87d466af 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -344,6 +344,13 @@ SOFTWARE.
Copyright (c) 2018, Jaisel Rahman +
  • + diff-match-patch +
    + Copyright 2018 The diff-match-patch Authors. https://github.com/google/diff-match-patch +
  • + +
     Apache License
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
    index 06f6512939..611ee171bd 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
    @@ -35,10 +35,7 @@ import im.vector.riotx.features.crypto.verification.SASVerificationIncomingFragm
     import im.vector.riotx.features.home.*
     import im.vector.riotx.features.home.group.GroupListFragment
     import im.vector.riotx.features.home.room.detail.RoomDetailFragment
    -import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
    -import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuFragment
    -import im.vector.riotx.features.home.room.detail.timeline.action.QuickReactionFragment
    -import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet
    +import im.vector.riotx.features.home.room.detail.timeline.action.*
     import im.vector.riotx.features.home.room.list.RoomListFragment
     import im.vector.riotx.features.invite.VectorInviteView
     import im.vector.riotx.features.login.LoginActivity
    @@ -93,6 +90,8 @@ interface ScreenComponent {
     
         fun inject(viewReactionBottomSheet: ViewReactionBottomSheet)
     
    +    fun inject(viewEditHistoryBottomSheet: ViewEditHistoryBottomSheet)
    +
         fun inject(messageMenuFragment: MessageMenuFragment)
     
         fun inject(vectorSettingsActivity: VectorSettingsActivity)
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt
    index 234d4a0caf..37abde20b8 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt
    @@ -29,11 +29,7 @@ import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsVie
     import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsViewModel_AssistedFactory
     import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedViewModel
     import im.vector.riotx.features.crypto.verification.SasVerificationViewModel
    -import im.vector.riotx.features.home.HomeActivityViewModel
    -import im.vector.riotx.features.home.HomeActivityViewModel_AssistedFactory
    -import im.vector.riotx.features.home.HomeDetailViewModel
    -import im.vector.riotx.features.home.HomeDetailViewModel_AssistedFactory
    -import im.vector.riotx.features.home.HomeNavigationViewModel
    +import im.vector.riotx.features.home.*
     import im.vector.riotx.features.home.group.GroupListViewModel
     import im.vector.riotx.features.home.group.GroupListViewModel_AssistedFactory
     import im.vector.riotx.features.home.room.detail.RoomDetailViewModel
    @@ -59,7 +55,7 @@ import im.vector.riotx.features.workers.signout.SignOutViewModel
     
     @Module
     interface ViewModelModule {
    -    
    +
     
         @Binds
         fun bindViewModelFactory(factory: VectorViewModelFactory): ViewModelProvider.Factory
    @@ -156,6 +152,9 @@ interface ViewModelModule {
         @Binds
         fun bindViewReactionViewModelFactory(factory: ViewReactionViewModel_AssistedFactory): ViewReactionViewModel.Factory
     
    +    @Binds
    +    fun bindViewEditHistoryViewModelFactory(factory: ViewEditHistoryViewModel_AssistedFactory): ViewEditHistoryViewModel.Factory
    +
         @Binds
         fun bindCreateRoomViewModelFactory(factory: CreateRoomViewModel_AssistedFactory): CreateRoomViewModel.Factory
     
    diff --git a/vector/src/main/java/im/vector/riotx/core/ui/list/GenericItem.kt b/vector/src/main/java/im/vector/riotx/core/ui/list/GenericItem.kt
    index ebaeb2d39e..f0a62ccd5a 100644
    --- a/vector/src/main/java/im/vector/riotx/core/ui/list/GenericItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/ui/list/GenericItem.kt
    @@ -51,7 +51,7 @@ abstract class GenericItem : VectorEpoxyModel() {
         var title: String? = null
     
         @EpoxyAttribute
    -    var description: String? = null
    +    var description: CharSequence? = null
     
         @EpoxyAttribute
         var style: STYLE = STYLE.NORMAL_TEXT
    diff --git a/vector/src/main/java/im/vector/riotx/core/ui/list/GenericItemHeader.kt b/vector/src/main/java/im/vector/riotx/core/ui/list/GenericItemHeader.kt
    new file mode 100644
    index 0000000000..3c9ce20de3
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/core/ui/list/GenericItemHeader.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.riotx.core.ui.list
    +
    +import android.widget.TextView
    +import com.airbnb.epoxy.EpoxyAttribute
    +import com.airbnb.epoxy.EpoxyModelClass
    +import im.vector.riotx.R
    +import im.vector.riotx.core.epoxy.VectorEpoxyHolder
    +import im.vector.riotx.core.epoxy.VectorEpoxyModel
    +import im.vector.riotx.core.extensions.setTextOrHide
    +
    +/**
    + * A generic list item header left aligned with notice color.
    + */
    +@EpoxyModelClass(layout = R.layout.item_generic_header)
    +abstract class GenericItemHeader : VectorEpoxyModel() {
    +
    +    @EpoxyAttribute
    +    var text: String? = null
    +
    +    override fun bind(holder: Holder) {
    +        holder.text.setTextOrHide(text)
    +    }
    +
    +    class Holder : VectorEpoxyHolder() {
    +        val text by bind(R.id.itemGenericHeaderText)
    +    }
    +}
    \ No newline at end of file
    diff --git a/vector/src/main/java/im/vector/riotx/core/ui/list/GenericLoaderItem.kt b/vector/src/main/java/im/vector/riotx/core/ui/list/GenericLoaderItem.kt
    new file mode 100644
    index 0000000000..56daca223e
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/core/ui/list/GenericLoaderItem.kt
    @@ -0,0 +1,20 @@
    +package im.vector.riotx.core.ui.list
    +
    +import com.airbnb.epoxy.EpoxyModelClass
    +import im.vector.riotx.R
    +import im.vector.riotx.core.epoxy.VectorEpoxyHolder
    +import im.vector.riotx.core.epoxy.VectorEpoxyModel
    +
    +
    +/**
    + * A generic list item header left aligned with notice color.
    + */
    +@EpoxyModelClass(layout = R.layout.item_generic_loader)
    +abstract class GenericLoaderItem : VectorEpoxyModel() {
    +
    +    //Maybe/Later add some style configuration, SMALL/BIG ?
    +
    +    override fun bind(holder: Holder) {}
    +
    +    class Holder : VectorEpoxyHolder()
    +}
    \ No newline at end of file
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    index d52b16ca04..ace0802e09 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
    @@ -32,7 +32,6 @@ sealed class RoomDetailActions {
         data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions()
         data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions()
         data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions()
    -    data class ShowEditHistoryAction(val event: String, val editAggregatedSummary: EditAggregatedSummary) : RoomDetailActions()
         data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions()
         data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions()
         object AcceptInvite : RoomDetailActions()
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    index f611fe9973..943b6165e0 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    @@ -85,10 +85,7 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerView
     import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel
     import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
     import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
    -import im.vector.riotx.features.home.room.detail.timeline.action.ActionsHandler
    -import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
    -import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuViewModel
    -import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet
    +import im.vector.riotx.features.home.room.detail.timeline.action.*
     import im.vector.riotx.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
     import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
     import im.vector.riotx.features.html.EventHtmlRenderer
    @@ -666,10 +663,8 @@ class RoomDetailFragment :
         }
     
         override fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?) {
    -        editAggregatedSummary?.also {
    -            roomDetailViewModel.process(RoomDetailActions.ShowEditHistoryAction(informationData.eventId, it))
    -        }
    -
    +        ViewEditHistoryBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
    +                .show(requireActivity().supportFragmentManager, "DISPLAY_EDITS")
         }
     // AutocompleteUserPresenter.Callback
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    index 2a19914a8c..36b989fe31 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    @@ -114,7 +114,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                 is RoomDetailActions.RedactAction           -> handleRedactEvent(action)
                 is RoomDetailActions.UndoReaction           -> handleUndoReact(action)
                 is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
    -            is RoomDetailActions.ShowEditHistoryAction  -> handleShowEditHistoryReaction(action)
                 is RoomDetailActions.EnterEditMode          -> handleEditAction(action)
                 is RoomDetailActions.EnterQuoteMode         -> handleQuoteAction(action)
                 is RoomDetailActions.EnterReplyMode         -> handleReplyAction(action)
    @@ -309,22 +308,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
             return finalText
         }
     
    -    private fun handleShowEditHistoryReaction(action: RoomDetailActions.ShowEditHistoryAction) {
    -        //TODO temporary implementation
    -        val lastReplace = action.editAggregatedSummary.sourceEvents.lastOrNull()?.let {
    -            room.getTimeLineEvent(it)
    -        } ?: return
    -
    -        val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault())
    -        _nonBlockingPopAlert.postValue(LiveEvent(
    -                Pair(R.string.last_edited_info_message, listOf(
    -                        lastReplace.getDisambiguatedDisplayName(),
    -                        dateFormat.format(Date(lastReplace.root.originServerTs ?: 0)))
    -                ))
    -        )
    -    }
    -
    -
         private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
             _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled))
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt
    new file mode 100644
    index 0000000000..aefbde431a
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt
    @@ -0,0 +1,93 @@
    +/*
    + * 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.riotx.features.home.room.detail.timeline.action
    +
    +import android.os.Bundle
    +import android.view.LayoutInflater
    +import android.view.View
    +import android.view.ViewGroup
    +import android.widget.LinearLayout
    +import androidx.recyclerview.widget.DividerItemDecoration
    +import butterknife.BindView
    +import butterknife.ButterKnife
    +import com.airbnb.epoxy.EpoxyRecyclerView
    +import com.airbnb.mvrx.MvRx
    +import com.airbnb.mvrx.fragmentViewModel
    +import com.airbnb.mvrx.withState
    +import im.vector.riotx.R
    +import im.vector.riotx.core.di.ScreenComponent
    +import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
    +import im.vector.riotx.features.html.EventHtmlRenderer
    +import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
    +import javax.inject.Inject
    +
    +
    +/**
    + * Bottom sheet displaying list of edits for a given event ordered by timestamp
    + */
    +class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
    +
    +    private val viewModel: ViewEditHistoryViewModel by fragmentViewModel(ViewEditHistoryViewModel::class)
    +
    +    @Inject lateinit var viewEditHistoryViewModelFactory: ViewEditHistoryViewModel.Factory
    +    @Inject lateinit var eventHtmlRenderer: EventHtmlRenderer
    +
    +    @BindView(R.id.bottom_sheet_display_reactions_list)
    +    lateinit var epoxyRecyclerView: EpoxyRecyclerView
    +
    +    private val epoxyController by lazy {
    +        ViewEditHistoryEpoxyController(requireContext(), viewModel.timelineDateFormatter, eventHtmlRenderer)
    +    }
    +
    +    override fun injectWith(screenComponent: ScreenComponent) {
    +        screenComponent.inject(this)
    +    }
    +
    +    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    +        val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false)
    +        ButterKnife.bind(this, view)
    +        return view
    +    }
    +
    +    override fun onActivityCreated(savedInstanceState: Bundle?) {
    +        super.onActivityCreated(savedInstanceState)
    +        epoxyRecyclerView.setController(epoxyController)
    +        val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
    +                LinearLayout.VERTICAL)
    +        epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
    +        bottomSheetTitle.text = context?.getString(R.string.message_edits)
    +    }
    +
    +
    +    override fun invalidate() = withState(viewModel) {
    +        epoxyController.setData(it)
    +    }
    +
    +    companion object {
    +        fun newInstance(roomId: String, informationData: MessageInformationData): ViewEditHistoryBottomSheet {
    +            val args = Bundle()
    +            val parcelableArgs = TimelineEventFragmentArgs(
    +                    informationData.eventId,
    +                    roomId,
    +                    informationData
    +            )
    +            args.putParcelable(MvRx.KEY_ARG, parcelableArgs)
    +            return ViewEditHistoryBottomSheet().apply { arguments = args }
    +
    +        }
    +    }
    +}
    +
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt
    new file mode 100644
    index 0000000000..4ae62fbd93
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt
    @@ -0,0 +1,155 @@
    +/*
    + * 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.riotx.features.home.room.detail.timeline.action
    +
    +import android.content.Context
    +import android.text.Spannable
    +import android.text.format.DateUtils
    +import androidx.core.content.ContextCompat
    +import com.airbnb.epoxy.TypedEpoxyController
    +import com.airbnb.mvrx.Fail
    +import com.airbnb.mvrx.Incomplete
    +import com.airbnb.mvrx.Success
    +import im.vector.matrix.android.api.session.events.model.Event
    +import im.vector.matrix.android.api.session.events.model.toModel
    +import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
    +import im.vector.riotx.R
    +import im.vector.riotx.core.extensions.localDateTime
    +import im.vector.riotx.core.ui.list.genericFooterItem
    +import im.vector.riotx.core.ui.list.genericItem
    +import im.vector.riotx.core.ui.list.genericItemHeader
    +import im.vector.riotx.core.ui.list.genericLoaderItem
    +import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter
    +import im.vector.riotx.features.html.EventHtmlRenderer
    +import me.gujun.android.span.span
    +import name.fraser.neil.plaintext.diff_match_patch
    +import timber.log.Timber
    +import java.util.*
    +
    +/**
    + * Epoxy controller for reaction event list
    + */
    +class ViewEditHistoryEpoxyController(private val context: Context,
    +                                     val timelineDateFormatter: TimelineDateFormatter,
    +                                     val eventHtmlRenderer: EventHtmlRenderer) : TypedEpoxyController() {
    +
    +    override fun buildModels(state: ViewEditHistoryViewState) {
    +        when (state.editList) {
    +            is Incomplete -> {
    +                genericLoaderItem {
    +                    id("Spinner")
    +                }
    +            }
    +            is Fail       -> {
    +                genericFooterItem {
    +                    id("failure")
    +                    text(context.getString(R.string.unknown_error))
    +                }
    +            }
    +            is Success    -> {
    +                state.editList()?.let { renderEvents(it) }
    +            }
    +
    +        }
    +    }
    +
    +    private fun renderEvents(sourceEvents: List) {
    +        if (sourceEvents.isEmpty()) {
    +            genericItem {
    +                id("footer")
    +                title(context.getString(R.string.no_message_edits_found))
    +            }
    +        } else {
    +            var lastDate: Calendar? = null
    +            sourceEvents.sortedByDescending {
    +                it.originServerTs ?: 0
    +            }.forEachIndexed { index, timelineEvent ->
    +
    +                val evDate = Calendar.getInstance().apply {
    +                    timeInMillis = timelineEvent.originServerTs
    +                            ?: System.currentTimeMillis()
    +                }
    +                if (lastDate?.get(Calendar.DAY_OF_YEAR) != evDate.get(Calendar.DAY_OF_YEAR)) {
    +                    //need to display header with day
    +                    val dateString = if (DateUtils.isToday(evDate.timeInMillis)) context.getString(R.string.today)
    +                    else timelineDateFormatter.formatMessageDay(timelineEvent.localDateTime())
    +                    genericItemHeader {
    +                        id(evDate.hashCode())
    +                        text(dateString)
    +                    }
    +                }
    +                lastDate = evDate
    +                val cContent = getCorrectContent(timelineEvent)
    +                val body = cContent.second?.let { eventHtmlRenderer.render(it) }
    +                        ?: cContent.first
    +
    +                val nextEvent = if (index + 1 <= sourceEvents.lastIndex) sourceEvents[index + 1] else null
    +
    +                var spannedDiff: Spannable? = null
    +                if (nextEvent != null && cContent.second == null /*No diff for html*/) {
    +                    //compares the body
    +                    val nContent = getCorrectContent(nextEvent)
    +                    val nextBody = nContent.second?.let { eventHtmlRenderer.render(it) }
    +                            ?: nContent.first
    +                    val dmp = diff_match_patch()
    +                    val diff = dmp.diff_main(nextBody.toString(), body.toString())
    +                    Timber.e("#### Diff: $diff")
    +                    dmp.diff_cleanupSemantic(diff)
    +                    Timber.e("#### Diff: $diff")
    +                    spannedDiff = span {
    +                        diff.map {
    +                            when (it.operation) {
    +                                diff_match_patch.Operation.DELETE -> {
    +                                    span {
    +                                        text = it.text
    +                                        textColor = ContextCompat.getColor(context, R.color.vector_error_color)
    +                                        textDecorationLine = "line-through"
    +                                    }
    +                                }
    +                                diff_match_patch.Operation.INSERT -> {
    +                                    span {
    +                                        text = it.text
    +                                        textColor = ContextCompat.getColor(context, R.color.vector_success_color)
    +                                    }
    +                                }
    +                                else                              -> {
    +                                    span {
    +                                        text = it.text
    +                                    }
    +                                }
    +                            }
    +                        }
    +
    +                    }
    +                }
    +                genericItem {
    +                    id(timelineEvent.eventId)
    +                    title(timelineDateFormatter.formatMessageHour(timelineEvent.localDateTime()))
    +                    description(spannedDiff ?: body)
    +                }
    +            }
    +        }
    +    }
    +
    +    private fun getCorrectContent(event: Event): Pair {
    +        val clearContent = event.getClearContent().toModel()
    +        val newContent = clearContent
    +                ?.newContent
    +                ?.toModel()
    +        return (newContent?.body ?: clearContent?.body ?: "") to (newContent?.formattedBody
    +                ?: clearContent?.formattedBody)
    +    }
    +}
    \ No newline at end of file
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt
    new file mode 100644
    index 0000000000..64005c3fa3
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt
    @@ -0,0 +1,90 @@
    +/*
    + * 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.riotx.features.home.room.detail.timeline.action
    +
    +import com.airbnb.mvrx.*
    +import com.squareup.inject.assisted.Assisted
    +import com.squareup.inject.assisted.AssistedInject
    +import im.vector.matrix.android.api.MatrixCallback
    +import im.vector.matrix.android.api.session.Session
    +import im.vector.matrix.android.api.session.events.model.Event
    +import im.vector.riotx.core.platform.VectorViewModel
    +import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter
    +
    +
    +data class ViewEditHistoryViewState(
    +        val eventId: String,
    +        val roomId: String,
    +        val editList: Async> = Uninitialized)
    +    : MvRxState {
    +
    +    constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId)
    +
    +}
    +
    +class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted
    +                                                           initialState: ViewEditHistoryViewState,
    +                                                           val session: Session,
    +                                                           val timelineDateFormatter: TimelineDateFormatter
    +) : VectorViewModel(initialState) {
    +
    +    private val roomId = initialState.roomId
    +    private val eventId = initialState.eventId
    +    private val room = session.getRoom(roomId)
    +            ?: throw IllegalStateException("Shouldn't use this ViewModel without a room")
    +
    +    @AssistedInject.Factory
    +    interface Factory {
    +        fun create(initialState: ViewEditHistoryViewState): ViewEditHistoryViewModel
    +    }
    +
    +
    +    companion object : MvRxViewModelFactory {
    +
    +        override fun create(viewModelContext: ViewModelContext, state: ViewEditHistoryViewState): ViewEditHistoryViewModel? {
    +            val fragment: ViewEditHistoryBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
    +            return fragment.viewEditHistoryViewModelFactory.create(state)
    +        }
    +
    +    }
    +
    +    init {
    +        loadHistory()
    +    }
    +
    +    private fun loadHistory() {
    +        setState { copy(editList = Loading()) }
    +        room.fetchEditHistory(eventId, object : MatrixCallback> {
    +            override fun onFailure(failure: Throwable) {
    +                setState {
    +                    copy(editList = Fail(failure))
    +                }
    +            }
    +
    +            override fun onSuccess(data: List) {
    +                //TODO until supported by API Add original event manually
    +                val withOriginal = data.toMutableList()
    +                room.getTimeLineEvent(eventId)?.let {
    +                    withOriginal.add(it.root)
    +                }
    +                setState {
    +                    copy(editList = Success(withOriginal))
    +                }
    +            }
    +        })
    +    }
    +
    +}
    \ No newline at end of file
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt
    index 760b74daf6..d7e41784ea 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt
    @@ -21,7 +21,6 @@ import android.view.LayoutInflater
     import android.view.View
     import android.view.ViewGroup
     import android.widget.LinearLayout
    -import androidx.core.view.isVisible
     import androidx.recyclerview.widget.DividerItemDecoration
     import butterknife.BindView
     import butterknife.ButterKnife
    @@ -33,7 +32,7 @@ import im.vector.riotx.EmojiCompatFontProvider
     import im.vector.riotx.R
     import im.vector.riotx.core.di.ScreenComponent
     import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
    -import kotlinx.android.synthetic.main.bottom_sheet_display_reactions.*
    +import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
     import javax.inject.Inject
     
     /**
    @@ -49,14 +48,16 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() {
         @BindView(R.id.bottom_sheet_display_reactions_list)
         lateinit var epoxyRecyclerView: EpoxyRecyclerView
     
    -    private val epoxyController by lazy { ViewReactionsEpoxyController(emojiCompatFontProvider.typeface) }
    +    private val epoxyController by lazy {
    +        ViewReactionsEpoxyController(requireContext(), emojiCompatFontProvider.typeface)
    +    }
     
         override fun injectWith(screenComponent: ScreenComponent) {
             screenComponent.inject(this)
         }
     
         override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    -        val view = inflater.inflate(R.layout.bottom_sheet_display_reactions, container, false)
    +        val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false)
             ButterKnife.bind(this, view)
             return view
         }
    @@ -67,16 +68,11 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() {
             val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
                     LinearLayout.VERTICAL)
             epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
    +        bottomSheetTitle.text = context?.getString(R.string.reactions)
         }
     
     
         override fun invalidate() = withState(viewModel) {
    -        if (it.mapReactionKeyToMemberList() == null) {
    -            bottomSheetViewReactionSpinner.isVisible = true
    -            bottomSheetViewReactionSpinner.animate()
    -        } else {
    -            bottomSheetViewReactionSpinner.isVisible = false
    -        }
             epoxyController.setData(it)
         }
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt
    index 57c3d26528..74b3f4925f 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt
    @@ -16,24 +16,47 @@
     
     package im.vector.riotx.features.home.room.detail.timeline.action
     
    +import android.content.Context
     import android.graphics.Typeface
     import com.airbnb.epoxy.TypedEpoxyController
    +import com.airbnb.mvrx.Fail
    +import com.airbnb.mvrx.Incomplete
    +import com.airbnb.mvrx.Success
    +import im.vector.riotx.R
    +import im.vector.riotx.core.ui.list.genericFooterItem
    +import im.vector.riotx.core.ui.list.genericLoaderItem
     
     /**
      * Epoxy controller for reaction event list
      */
    -class ViewReactionsEpoxyController(private val emojiCompatTypeface: Typeface?) : TypedEpoxyController() {
    +class ViewReactionsEpoxyController(private val context: Context, private val emojiCompatTypeface: Typeface?)
    +    : TypedEpoxyController() {
     
         override fun buildModels(state: DisplayReactionsViewState) {
    -        val map = state.mapReactionKeyToMemberList() ?: return
    -        map.forEach {
    -            reactionInfoSimpleItem {
    -                id(it.eventId)
    -                emojiTypeFace(emojiCompatTypeface)
    -                timeStamp(it.timestamp)
    -                reactionKey(it.reactionKey)
    -                authorDisplayName(it.authorName ?: it.authorId)
    +        when (state.mapReactionKeyToMemberList) {
    +            is Incomplete -> {
    +                genericLoaderItem {
    +                    id("Spinner")
    +                }
    +            }
    +            is Fail       -> {
    +                genericFooterItem {
    +                    id("failure")
    +                    text(context.getString(R.string.unknown_error))
    +                }
    +            }
    +            is Success    -> {
    +                state.mapReactionKeyToMemberList()?.forEach {
    +                    reactionInfoSimpleItem {
    +                        id(it.eventId)
    +                        emojiTypeFace(emojiCompatTypeface)
    +                        timeStamp(it.timestamp)
    +                        reactionKey(it.reactionKey)
    +                        authorDisplayName(it.authorName ?: it.authorId)
    +                    }
    +                }
                 }
             }
    +
         }
     }
    \ No newline at end of file
    diff --git a/vector/src/main/res/layout/bottom_sheet_display_reactions.xml b/vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml
    similarity index 70%
    rename from vector/src/main/res/layout/bottom_sheet_display_reactions.xml
    rename to vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml
    index 0f5b63654d..9b3ffb26a3 100644
    --- a/vector/src/main/res/layout/bottom_sheet_display_reactions.xml
    +++ b/vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml
    @@ -7,23 +7,14 @@
         android:orientation="vertical">
     
         
    -
    -    
    -
    +        android:textSize="16sp"
    +        tools:text="@string/reactions" />
     
         
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/layout/item_generic_loader.xml b/vector/src/main/res/layout/item_generic_loader.xml
    new file mode 100644
    index 0000000000..53460c4e7a
    --- /dev/null
    +++ b/vector/src/main/res/layout/item_generic_loader.xml
    @@ -0,0 +1,6 @@
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml
    index 7c1d39c4df..9a20356ed1 100644
    --- a/vector/src/main/res/values/strings_riotX.xml
    +++ b/vector/src/main/res/values/strings_riotX.xml
    @@ -21,4 +21,8 @@
         Use the old app
     
     
    +
    +    Message Edits
    +    No edits found
    +
     
    \ No newline at end of file