Allow using the latest user avatar and display name for all messages in the timeline

Signed-off-by: Jorge Martín Espinosa <jorgem@element.io>
This commit is contained in:
Ahmed Radhouane Belkilani 2022-03-30 10:42:52 +02:00 committed by Jorge Martín
parent f54c865cf4
commit 6a523ccc38
14 changed files with 145 additions and 14 deletions

1
changelog.d/5932.feature Normal file
View File

@ -0,0 +1 @@
Allow using the latest user Avatar and name for all messages in the timeline

View File

@ -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 * The root thread eventId if this is a thread timeline, or null if this is NOT a thread timeline
*/ */
val rootThreadEventId: String? = null, val rootThreadEventId: String? = null,
/**
* If true Sender Info shown in room will get the latest data information (avatar + displayName)
*/
val useLiveSenderInfo: Boolean = false,
) { ) {
/** /**

View File

@ -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.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask 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.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.ReadReceiptHandler
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
@ -59,6 +60,7 @@ internal class DefaultTimeline(
private val settings: TimelineSettings, private val settings: TimelineSettings,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val clock: Clock, private val clock: Clock,
stateEventDataSource: StateEventDataSource,
paginationTask: PaginationTask, paginationTask: PaginationTask,
getEventTask: GetContextOfEventTask, getEventTask: GetContextOfEventTask,
fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
@ -106,7 +108,9 @@ internal class DefaultTimeline(
onEventsUpdated = this::sendSignalToPostSnapshot, onEventsUpdated = this::sendSignalToPostSnapshot,
onEventsDeleted = this::onEventsDeleted, onEventsDeleted = this::onEventsDeleted,
onLimitedTimeline = this::onLimitedTimeline, onLimitedTimeline = this::onLimitedTimeline,
onNewTimelineEvents = this::onNewTimelineEvents onNewTimelineEvents = this::onNewTimelineEvents,
stateEventDataSource = stateEventDataSource,
matrixCoroutineDispatchers = coroutineDispatchers,
) )
private var strategy: LoadTimelineStrategy = buildStrategy(LoadTimelineStrategy.Mode.Live) private var strategy: LoadTimelineStrategy = buildStrategy(LoadTimelineStrategy.Mode.Live)

View File

@ -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.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask 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.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.ReadReceiptHandler
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.time.Clock
@ -53,6 +54,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val timelineEventDataSource: TimelineEventDataSource, private val timelineEventDataSource: TimelineEventDataSource,
private val clock: Clock, private val clock: Clock,
private val stateEventDataSource: StateEventDataSource,
) : TimelineService { ) : TimelineService {
@AssistedFactory @AssistedFactory
@ -78,7 +80,8 @@ internal class DefaultTimelineService @AssistedInject constructor(
getEventTask = contextOfEventTask, getEventTask = contextOfEventTask,
threadsAwarenessHandler = threadsAwarenessHandler, threadsAwarenessHandler = threadsAwarenessHandler,
lightweightSettingsStorage = lightweightSettingsStorage, lightweightSettingsStorage = lightweightSettingsStorage,
clock = clock clock = clock,
stateEventDataSource = stateEventDataSource,
) )
} }

View File

@ -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]
}

View File

@ -23,6 +23,7 @@ import io.realm.RealmConfiguration
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
import kotlinx.coroutines.CompletableDeferred 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.extensions.orFalse
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError 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.findLastForwardChunkOfThread
import org.matrix.android.sdk.internal.database.query.where 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.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.session.sync.handler.room.ThreadsAwarenessHandler
import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber import timber.log.Timber
@ -100,7 +102,9 @@ internal class LoadTimelineStrategy constructor(
val onEventsUpdated: (Boolean) -> Unit, val onEventsUpdated: (Boolean) -> Unit,
val onEventsDeleted: () -> Unit, val onEventsDeleted: () -> Unit,
val onLimitedTimeline: () -> 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 private var getContextLatch: CompletableDeferred<Unit>? = null
@ -165,7 +169,13 @@ internal class LoadTimelineStrategy constructor(
onEventsUpdated = dependencies.onEventsUpdated onEventsUpdated = dependencies.onEventsUpdated
) )
fun onStart() { private val liveRoomStateListener = LiveRoomStateListener(
roomId,
dependencies.stateEventDataSource,
dependencies.matrixCoroutineDispatchers.main
)
suspend fun onStart() {
dependencies.eventDecryptor.start() dependencies.eventDecryptor.start()
dependencies.timelineInput.listeners.add(timelineInputListener) dependencies.timelineInput.listeners.add(timelineInputListener)
val realm = dependencies.realm.get() val realm = dependencies.realm.get()
@ -174,9 +184,13 @@ internal class LoadTimelineStrategy constructor(
it.addChangeListener(chunkEntityListener) it.addChangeListener(chunkEntityListener)
timelineChunk = it.createTimelineChunk() timelineChunk = it.createTimelineChunk()
} }
if (dependencies.timelineSettings.useLiveSenderInfo) {
liveRoomStateListener.start()
}
} }
fun onStop() { suspend fun onStop() {
dependencies.eventDecryptor.destroy() dependencies.eventDecryptor.destroy()
dependencies.timelineInput.listeners.remove(timelineInputListener) dependencies.timelineInput.listeners.remove(timelineInputListener)
chunkEntity?.removeChangeListener(chunkEntityListener) chunkEntity?.removeChangeListener(chunkEntityListener)
@ -188,6 +202,9 @@ internal class LoadTimelineStrategy constructor(
if (mode is Mode.Thread) { if (mode is Mode.Thread) {
clearThreadChunkEntity(dependencies.realm.get(), mode.rootThreadEventId) clearThreadChunkEntity(dependencies.realm.get(), mode.rootThreadEventId)
} }
if (dependencies.timelineSettings.useLiveSenderInfo) {
liveRoomStateListener.stop()
}
} }
suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult { suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult {
@ -222,7 +239,22 @@ internal class LoadTimelineStrategy constructor(
} }
fun buildSnapshot(): List<TimelineEvent> { 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> { private fun buildSendingEvents(): List<TimelineEvent> {

View File

@ -136,6 +136,7 @@ internal class TimelineChunk(
val prevEvents = prevChunk?.builtItems(includesNext = false, includesPrev = true).orEmpty() val prevEvents = prevChunk?.builtItems(includesNext = false, includesPrev = true).orEmpty()
deepBuiltItems.addAll(prevEvents) deepBuiltItems.addAll(prevEvents)
} }
return deepBuiltItems return deepBuiltItems
} }

View File

@ -38,7 +38,8 @@
<!-- Level 1: Labs --> <!-- Level 1: Labs -->
<bool name="settings_labs_thread_messages_default">false</bool> <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: Advanced settings -->
<!-- Level 1: Help and about --> <!-- Level 1: Help and about -->

View File

@ -52,4 +52,8 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences:
fun areThreadMessagesEnabled(): Boolean { fun areThreadMessagesEnabled(): Boolean {
return vectorPreferences.areThreadMessagesEnabled() return vectorPreferences.areThreadMessagesEnabled()
} }
fun showLiveSenderInfo(): Boolean {
return vectorPreferences.showLiveSenderInfo()
}
} }

View File

@ -26,7 +26,8 @@ class TimelineSettingsFactory @Inject constructor(private val userPreferencesPro
return TimelineSettings( return TimelineSettings(
initialSize = 30, initialSize = 30,
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts(), buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts(),
rootThreadEventId = rootThreadEventId rootThreadEventId = rootThreadEventId,
useLiveSenderInfo = userPreferencesProvider.showLiveSenderInfo()
) )
} }
} }

View File

@ -211,6 +211,9 @@ class VectorPreferences @Inject constructor(
const val SETTINGS_LABS_ENABLE_THREAD_MESSAGES = "SETTINGS_LABS_ENABLE_THREAD_MESSAGES_FINAL" const val SETTINGS_LABS_ENABLE_THREAD_MESSAGES = "SETTINGS_LABS_ENABLE_THREAD_MESSAGES_FINAL"
const val SETTINGS_THREAD_MESSAGES_SYNCED = "SETTINGS_THREAD_MESSAGES_SYNCED" 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 // Possible values for TAKE_PHOTO_VIDEO_MODE
const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0 const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0
const val TAKE_PHOTO_VIDEO_MODE_PHOTO = 1 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) return defaultPrefs.getBoolean(SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE, true)
} }
/**
* Indicates whether or not thread messages are enabled
*/
fun areThreadMessagesEnabled(): Boolean { fun areThreadMessagesEnabled(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_THREAD_MESSAGES, getDefault(R.bool.settings_labs_thread_messages_default)) 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) .putBoolean(SETTINGS_THREAD_MESSAGES_SYNCED, shouldMigrate)
.apply() .apply()
} }
fun showLiveSenderInfo(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO, getDefault(R.bool.settings_timeline_show_live_sender_info_default))
}
} }

View File

@ -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_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">Enable Thread Messages</string>
<string name="labs_enable_thread_messages_desc">Note: app will be restarted</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> <string name="user_invites_you">%s invites you</string>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?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--> <!--<im.vector.app.core.preference.VectorPreferenceCategory-->
<!--android:key="SETTINGS_LABS_PREFERENCE_KEY"--> <!--android:key="SETTINGS_LABS_PREFERENCE_KEY"-->

View File

@ -88,6 +88,13 @@
android:title="@string/message_bubbles" android:title="@string/message_bubbles"
app:isPreferenceVisible="@bool/settings_interface_bubble_visible" /> 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 <im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="SETTINGS_SHOW_URL_PREVIEW_KEY" android:key="SETTINGS_SHOW_URL_PREVIEW_KEY"