From 6a523ccc3807190774b6df53f9218573e80168bd Mon Sep 17 00:00:00 2001
From: Ahmed Radhouane Belkilani <ahmed-radhouane.belkilani@niji.fr>
Date: Wed, 30 Mar 2022 10:42:52 +0200
Subject: [PATCH] Allow using the latest user avatar and display name for all
 messages in the timeline
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Jorge Martín Espinosa <jorgem@element.io>
---
 changelog.d/5932.feature                      |  1 +
 .../session/room/timeline/TimelineSettings.kt |  4 ++
 .../session/room/timeline/DefaultTimeline.kt  |  6 +-
 .../room/timeline/DefaultTimelineService.kt   |  5 +-
 .../room/timeline/LiveRoomStateListener.kt    | 66 +++++++++++++++++++
 .../room/timeline/LoadTimelineStrategy.kt     | 40 +++++++++--
 .../session/room/timeline/TimelineChunk.kt    |  1 +
 .../src/main/res/values/config-settings.xml   |  3 +-
 .../core/resources/UserPreferencesProvider.kt |  4 ++
 .../helper/TimelineSettingsFactory.kt         |  3 +-
 .../features/settings/VectorPreferences.kt    | 10 ++-
 vector/src/main/res/values/strings.xml        |  2 +
 .../src/main/res/xml/vector_settings_labs.xml |  5 +-
 .../res/xml/vector_settings_preferences.xml   |  9 ++-
 14 files changed, 145 insertions(+), 14 deletions(-)
 create mode 100644 changelog.d/5932.feature
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveRoomStateListener.kt

diff --git a/changelog.d/5932.feature b/changelog.d/5932.feature
new file mode 100644
index 0000000000..dcfc6615b0
--- /dev/null
+++ b/changelog.d/5932.feature
@@ -0,0 +1 @@
+Allow using the latest user Avatar and name for all messages in the timeline
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt
index b45f3ecb71..bdda23b8e2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt
@@ -32,6 +32,10 @@ data class TimelineSettings(
          * The root thread eventId if this is a thread timeline, or null if this is NOT a thread timeline
          */
         val rootThreadEventId: String? = null,
+        /**
+         * If true Sender Info shown in room will get the latest data information (avatar + displayName)
+         */
+        val useLiveSenderInfo: Boolean = false,
 ) {
 
     /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index ac2dfb101c..fad21c0918 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -39,6 +39,7 @@ import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
 import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
 import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
+import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
 import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
 import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
 import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
@@ -59,6 +60,7 @@ internal class DefaultTimeline(
         private val settings: TimelineSettings,
         private val coroutineDispatchers: MatrixCoroutineDispatchers,
         private val clock: Clock,
+        stateEventDataSource: StateEventDataSource,
         paginationTask: PaginationTask,
         getEventTask: GetContextOfEventTask,
         fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
@@ -106,7 +108,9 @@ internal class DefaultTimeline(
             onEventsUpdated = this::sendSignalToPostSnapshot,
             onEventsDeleted = this::onEventsDeleted,
             onLimitedTimeline = this::onLimitedTimeline,
-            onNewTimelineEvents = this::onNewTimelineEvents
+            onNewTimelineEvents = this::onNewTimelineEvents,
+            stateEventDataSource = stateEventDataSource,
+            matrixCoroutineDispatchers = coroutineDispatchers,
     )
 
     private var strategy: LoadTimelineStrategy = buildStrategy(LoadTimelineStrategy.Mode.Live)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt
index 6d63b24cf5..53c0253876 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt
@@ -32,6 +32,7 @@ import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
 import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
+import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
 import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
 import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
 import org.matrix.android.sdk.internal.util.time.Clock
@@ -53,6 +54,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
         private val coroutineDispatchers: MatrixCoroutineDispatchers,
         private val timelineEventDataSource: TimelineEventDataSource,
         private val clock: Clock,
+        private val stateEventDataSource: StateEventDataSource,
 ) : TimelineService {
 
     @AssistedFactory
@@ -78,7 +80,8 @@ internal class DefaultTimelineService @AssistedInject constructor(
                 getEventTask = contextOfEventTask,
                 threadsAwarenessHandler = threadsAwarenessHandler,
                 lightweightSettingsStorage = lightweightSettingsStorage,
-                clock = clock
+                clock = clock,
+                stateEventDataSource = stateEventDataSource,
         )
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveRoomStateListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveRoomStateListener.kt
new file mode 100644
index 0000000000..b2692bf805
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveRoomStateListener.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.session.room.timeline
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+import org.matrix.android.sdk.api.query.QueryStringValue
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
+import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
+import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
+
+/**
+ * Helper to observe and query the live room state.
+ */
+internal class LiveRoomStateListener(
+        roomId: String,
+        stateEventDataSource: StateEventDataSource,
+        private val mainDispatcher: CoroutineDispatcher,
+) {
+    private val roomStateObserver = Observer<List<Event>> { stateEvents ->
+        stateEvents.map { event ->
+            val memberContent = event.getFixedRoomMemberContent() ?: return@map
+            val stateKey = event.stateKey ?: return@map
+            liveRoomState[stateKey] = memberContent
+        }
+    }
+    private val stateEventsLiveData: LiveData<List<Event>> by lazy {
+        stateEventDataSource.getStateEventsLive(
+                roomId = roomId,
+                eventTypes = setOf(EventType.STATE_ROOM_MEMBER),
+                stateKey = QueryStringValue.NoCondition,
+        )
+    }
+
+    private val liveRoomState = mutableMapOf<String, RoomMemberContent>()
+
+    suspend fun start() = withContext(mainDispatcher) {
+        stateEventsLiveData.observeForever(roomStateObserver)
+    }
+
+    suspend fun stop() = withContext(mainDispatcher) {
+        if (stateEventsLiveData.hasActiveObservers()) {
+            stateEventsLiveData.removeObserver(roomStateObserver)
+        }
+    }
+
+    fun getLiveState(stateKey: String): RoomMemberContent? = liveRoomState[stateKey]
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
index bcf202962c..1e5c993dfb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
@@ -23,6 +23,7 @@ import io.realm.RealmConfiguration
 import io.realm.RealmResults
 import io.realm.kotlin.createObject
 import kotlinx.coroutines.CompletableDeferred
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.failure.MatrixError
@@ -41,6 +42,7 @@ import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents
 import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
+import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
 import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
 import org.matrix.android.sdk.internal.util.time.Clock
 import timber.log.Timber
@@ -100,7 +102,9 @@ internal class LoadTimelineStrategy constructor(
             val onEventsUpdated: (Boolean) -> Unit,
             val onEventsDeleted: () -> Unit,
             val onLimitedTimeline: () -> Unit,
-            val onNewTimelineEvents: (List<String>) -> Unit
+            val onNewTimelineEvents: (List<String>) -> Unit,
+            val stateEventDataSource: StateEventDataSource,
+            val matrixCoroutineDispatchers: MatrixCoroutineDispatchers,
     )
 
     private var getContextLatch: CompletableDeferred<Unit>? = null
@@ -165,7 +169,13 @@ internal class LoadTimelineStrategy constructor(
             onEventsUpdated = dependencies.onEventsUpdated
     )
 
-    fun onStart() {
+    private val liveRoomStateListener = LiveRoomStateListener(
+            roomId,
+            dependencies.stateEventDataSource,
+            dependencies.matrixCoroutineDispatchers.main
+    )
+
+    suspend fun onStart() {
         dependencies.eventDecryptor.start()
         dependencies.timelineInput.listeners.add(timelineInputListener)
         val realm = dependencies.realm.get()
@@ -174,9 +184,13 @@ internal class LoadTimelineStrategy constructor(
             it.addChangeListener(chunkEntityListener)
             timelineChunk = it.createTimelineChunk()
         }
+
+        if (dependencies.timelineSettings.useLiveSenderInfo) {
+            liveRoomStateListener.start()
+        }
     }
 
-    fun onStop() {
+    suspend fun onStop() {
         dependencies.eventDecryptor.destroy()
         dependencies.timelineInput.listeners.remove(timelineInputListener)
         chunkEntity?.removeChangeListener(chunkEntityListener)
@@ -188,6 +202,9 @@ internal class LoadTimelineStrategy constructor(
         if (mode is Mode.Thread) {
             clearThreadChunkEntity(dependencies.realm.get(), mode.rootThreadEventId)
         }
+        if (dependencies.timelineSettings.useLiveSenderInfo) {
+            liveRoomStateListener.stop()
+        }
     }
 
     suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult {
@@ -222,7 +239,22 @@ internal class LoadTimelineStrategy constructor(
     }
 
     fun buildSnapshot(): List<TimelineEvent> {
-        return buildSendingEvents() + timelineChunk?.builtItems(includesNext = true, includesPrev = true).orEmpty()
+        val events = buildSendingEvents() + timelineChunk?.builtItems(includesNext = true, includesPrev = true).orEmpty()
+        return if (dependencies.timelineSettings.useLiveSenderInfo) {
+            events.map(this::applyLiveRoomState)
+        } else {
+            events
+        }
+    }
+
+    private fun applyLiveRoomState(event: TimelineEvent): TimelineEvent {
+        val updatedState = liveRoomStateListener.getLiveState(event.senderInfo.userId)
+        return if (updatedState != null) {
+            val updatedSenderInfo = event.senderInfo.copy(avatarUrl = updatedState.avatarUrl, displayName = updatedState.displayName)
+            event.copy(senderInfo = updatedSenderInfo)
+        } else {
+            event
+        }
     }
 
     private fun buildSendingEvents(): List<TimelineEvent> {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
index 8541c478ba..2c6218443c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
@@ -136,6 +136,7 @@ internal class TimelineChunk(
             val prevEvents = prevChunk?.builtItems(includesNext = false, includesPrev = true).orEmpty()
             deepBuiltItems.addAll(prevEvents)
         }
+
         return deepBuiltItems
     }
 
diff --git a/vector-config/src/main/res/values/config-settings.xml b/vector-config/src/main/res/values/config-settings.xml
index 0a4da4c98e..b2cd21c3de 100755
--- a/vector-config/src/main/res/values/config-settings.xml
+++ b/vector-config/src/main/res/values/config-settings.xml
@@ -38,7 +38,8 @@
 
     <!-- Level 1: Labs -->
     <bool name="settings_labs_thread_messages_default">false</bool>
-
+    <bool name="settings_timeline_show_live_sender_info_visible">true</bool>
+    <bool name="settings_timeline_show_live_sender_info_default">false</bool>
     <!-- Level 1: Advanced settings -->
 
     <!-- Level 1: Help and about -->
diff --git a/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt b/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt
index 3aa1964d8d..d39dcbe318 100644
--- a/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt
+++ b/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt
@@ -52,4 +52,8 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences:
     fun areThreadMessagesEnabled(): Boolean {
         return vectorPreferences.areThreadMessagesEnabled()
     }
+
+    fun showLiveSenderInfo(): Boolean {
+        return vectorPreferences.showLiveSenderInfo()
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt
index 8b7dcc9c72..18bdc2fdaa 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt
@@ -26,7 +26,8 @@ class TimelineSettingsFactory @Inject constructor(private val userPreferencesPro
         return TimelineSettings(
                 initialSize = 30,
                 buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts(),
-                rootThreadEventId = rootThreadEventId
+                rootThreadEventId = rootThreadEventId,
+                useLiveSenderInfo = userPreferencesProvider.showLiveSenderInfo()
         )
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
index ca42a07d50..15eac2a4ca 100755
--- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt
@@ -211,6 +211,9 @@ class VectorPreferences @Inject constructor(
         const val SETTINGS_LABS_ENABLE_THREAD_MESSAGES = "SETTINGS_LABS_ENABLE_THREAD_MESSAGES_FINAL"
         const val SETTINGS_THREAD_MESSAGES_SYNCED = "SETTINGS_THREAD_MESSAGES_SYNCED"
 
+        // This key will be used to enable user for displaying live user info or not.
+        const val SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO = "SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO"
+
         // Possible values for TAKE_PHOTO_VIDEO_MODE
         const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0
         const val TAKE_PHOTO_VIDEO_MODE_PHOTO = 1
@@ -1039,9 +1042,6 @@ class VectorPreferences @Inject constructor(
         return defaultPrefs.getBoolean(SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE, true)
     }
 
-    /**
-     * Indicates whether or not thread messages are enabled
-     */
     fun areThreadMessagesEnabled(): Boolean {
         return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_THREAD_MESSAGES, getDefault(R.bool.settings_labs_thread_messages_default))
     }
@@ -1091,4 +1091,8 @@ class VectorPreferences @Inject constructor(
                 .putBoolean(SETTINGS_THREAD_MESSAGES_SYNCED, shouldMigrate)
                 .apply()
     }
+
+    fun showLiveSenderInfo(): Boolean {
+        return defaultPrefs.getBoolean(SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO, getDefault(R.bool.settings_timeline_show_live_sender_info_default))
+    }
 }
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index e6568975e2..1868d1ff5b 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -2852,6 +2852,8 @@
     <string name="labs_auto_report_uisi_desc">Your system will automatically send logs when an unable to decrypt error occurs</string>
     <string name="labs_enable_thread_messages">Enable Thread Messages</string>
     <string name="labs_enable_thread_messages_desc">Note: app will be restarted</string>
+    <string name="settings_show_latest_profile">Show latest user info</string>
+    <string name="settings_show_latest_profile_description">Show the latest profile info (avatar and display name) for all the messages.</string>
 
     <string name="user_invites_you">%s invites you</string>
 
diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml
index 5144f6fe1f..8b25a8c287 100644
--- a/vector/src/main/res/xml/vector_settings_labs.xml
+++ b/vector/src/main/res/xml/vector_settings_labs.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
-<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
 
     <!--<im.vector.app.core.preference.VectorPreferenceCategory-->
     <!--android:key="SETTINGS_LABS_PREFERENCE_KEY"-->
@@ -68,4 +69,4 @@
         android:key="SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE"
         android:title="@string/labs_render_locations_in_timeline" />
 
-</androidx.preference.PreferenceScreen>
\ No newline at end of file
+</androidx.preference.PreferenceScreen>
diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml
index 6c8250a76a..7ff588ca76 100644
--- a/vector/src/main/res/xml/vector_settings_preferences.xml
+++ b/vector/src/main/res/xml/vector_settings_preferences.xml
@@ -88,6 +88,13 @@
             android:title="@string/message_bubbles"
             app:isPreferenceVisible="@bool/settings_interface_bubble_visible" />
 
+        <im.vector.app.core.preference.VectorSwitchPreference
+            android:defaultValue="@bool/settings_timeline_show_live_sender_info_default"
+            app:isPreferenceVisible="@bool/settings_timeline_show_live_sender_info_visible"
+            android:key="SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO"
+            android:summary="@string/settings_show_latest_profile_description"
+            android:title="@string/settings_show_latest_profile" />
+
         <im.vector.app.core.preference.VectorSwitchPreference
             android:defaultValue="true"
             android:key="SETTINGS_SHOW_URL_PREVIEW_KEY"
@@ -218,4 +225,4 @@
             android:title="@string/settings_room_directory_show_all_rooms" />
 
     </im.vector.app.core.preference.VectorPreferenceCategory>
-</androidx.preference.PreferenceScreen>
\ No newline at end of file
+</androidx.preference.PreferenceScreen>