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> { stateEvents -> + stateEvents.map { event -> + val memberContent = event.getFixedRoomMemberContent() ?: return@map + val stateKey = event.stateKey ?: return@map + liveRoomState[stateKey] = memberContent + } + } + private val stateEventsLiveData: LiveData> by lazy { + stateEventDataSource.getStateEventsLive( + roomId = roomId, + eventTypes = setOf(EventType.STATE_ROOM_MEMBER), + stateKey = QueryStringValue.NoCondition, + ) + } + + private val liveRoomState = mutableMapOf() + + 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) -> Unit + val onNewTimelineEvents: (List) -> Unit, + val stateEventDataSource: StateEventDataSource, + val matrixCoroutineDispatchers: MatrixCoroutineDispatchers, ) private var getContextLatch: CompletableDeferred? = 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 { - 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 { 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 @@ false - + true + false 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 @@ Your system will automatically send logs when an unable to decrypt error occurs Enable Thread Messages Note: app will be restarted + Show latest user info + Show the latest profile info (avatar and display name) for all the messages. %s invites you 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 @@ - + @@ -68,4 +69,4 @@ android:key="SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE" android:title="@string/labs_render_locations_in_timeline" /> - \ No newline at end of file + 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" /> + + - \ No newline at end of file +