From 51aa8208e0e65d71393a964cbceb0b8f5d033100 Mon Sep 17 00:00:00 2001 From: bxdxnn <267911624+bxdxnn@users.noreply.github.com> Date: Tue, 26 May 2026 07:36:57 +0000 Subject: [PATCH 1/5] Add `/myroomnick` slash command --- .../impl/timeline/TimelinePresenter.kt | 27 +++++++++---- .../factories/TimelineItemsFactory.kt | 39 +++++++++++++++++++ .../libraries/matrix/api/room/JoinedRoom.kt | 7 ++++ .../matrix/impl/room/JoinedRustRoom.kt | 6 +++ .../matrix/test/room/FakeJoinedRoom.kt | 5 +++ .../libraries/slashcommands/impl/Command.kt | 2 +- .../slashcommands/impl/CommandExecutor.kt | 6 +-- .../slashcommands/impl/CommandExecutorTest.kt | 16 ++++++-- 8 files changed, 92 insertions(+), 16 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 0b99c45e06d..3af7326ff51 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -51,6 +51,7 @@ import io.element.android.libraries.matrix.api.core.asEventId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState import io.element.android.libraries.matrix.api.room.roomMembers +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin @@ -254,15 +255,25 @@ class TimelinePresenter( } .launchIn(this) + var previousItems: List? = null combine(timelineController.timelineItems(), room.membersStateFlow) { items, membersState -> - val parent = analyticsService.getLongRunningTransaction(DisplayFirstTimelineItems) - val transaction = parent?.startChild("timelineItemsFactory.replaceWith", "Processing timeline items") - transaction?.putExtraData(AnalyticsUserData.TIMELINE_ITEM_COUNT, items.count().toString()) - timelineItemsFactory.replaceWith( - timelineItems = items, - roomMembers = membersState.roomMembers().orEmpty() - ) - transaction?.finish() + val roomMembers = membersState.roomMembers().orEmpty() + if (previousItems !== items) { + previousItems = items + val parent = analyticsService.getLongRunningTransaction(DisplayFirstTimelineItems) + val transaction = parent?.startChild("timelineItemsFactory.replaceWith", "Processing timeline items") + transaction?.putExtraData(AnalyticsUserData.TIMELINE_ITEM_COUNT, items.count().toString()) + timelineItemsFactory.replaceWith( + timelineItems = items, + roomMembers = roomMembers + ) + transaction?.finish() + } else { + timelineItemsFactory.updateRoomMembers( + timelineItems = items, + roomMembers = roomMembers + ) + } items } .onEach(redactedVoiceMessageManager::onEachMatrixTimelineItem) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt index dc8bdddc923..d1ec5b648a9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt @@ -72,6 +72,45 @@ class TimelineItemsFactory( } } + /** + * Lightweight update that only refreshes member-derived data (e.g., read receipt display names) + * on cached items without rebuilding the diff cache or creating new items. + * Skips emission if no cached items have member-dependent state. + */ + suspend fun updateRoomMembers( + timelineItems: List, + roomMembers: List, + ) = withContext(dispatchers.computation) { + lock.withLock { + var hasUpdates = false + val updatedStates = ArrayList() + for (index in diffCache.indices().reversed()) { + val cacheItem = diffCache.get(index) + if (cacheItem is TimelineItem.Event && roomMembers.isNotEmpty()) { + val updatedItem = eventItemFactory.update( + timelineItem = cacheItem, + receivedMatrixTimelineItem = timelineItems[index] as MatrixTimelineItem.Event, + roomMembers = roomMembers + ) + diffCache[index] = updatedItem + hasUpdates = true + updatedStates.add(updatedItem) + } else if (cacheItem != null) { + updatedStates.add(cacheItem) + } else { + buildAndCacheItem(timelineItems, index, roomMembers)?.also { timelineItemState -> + updatedStates.add(timelineItemState) + } + } + } + if (hasUpdates) { + val result = timelineItemGrouper.group(updatedStates).toImmutableList() + val filteredResult = filterEmptyDaySeparators(result) + _timelineItems.emit(filteredResult) + } + } + } + private suspend fun buildAndEmitTimelineItemStates( timelineItems: List, roomMembers: List, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt index f3fefab9a8d..6d8325427ba 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt @@ -212,4 +212,11 @@ interface JoinedRoom : BaseRoom { * @return Result indicating success or failure. */ suspend fun sendLiveLocation(geoUri: String): Result + + /** + * Sets the display name of the current user within this room. + * This is different from the global setDisplayName which updates + * the user's display name across all of their rooms. + */ + suspend fun setOwnMemberDisplayName(displayName: String): Result } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt index e75091eadcb..5d85a6a625d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt @@ -546,6 +546,12 @@ class JoinedRustRoom( } } + override suspend fun setOwnMemberDisplayName(displayName: String): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.setOwnMemberDisplayName(displayName) + } + } + override fun close() = destroy() override fun destroy() { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeJoinedRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeJoinedRoom.kt index b4425ddd4b2..dd8acacb236 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeJoinedRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeJoinedRoom.kt @@ -92,6 +92,7 @@ class FakeJoinedRoom( private val startLiveLocationShareResult: (Long) -> Result = { lambdaError() }, private val stopLiveLocationShareResult: () -> Result = { lambdaError() }, private val sendLiveLocationResult: (String) -> Result = { lambdaError() }, + private val setOwnMemberDisplayNameResult: (String) -> Result = { lambdaError() }, ) : JoinedRoom, BaseRoom by baseRoom { private val sendQueueUpdates = MutableSharedFlow(extraBufferCapacity = 10) @@ -255,6 +256,10 @@ class FakeJoinedRoom( sendLiveLocationResult(geoUri) } + override suspend fun setOwnMemberDisplayName(displayName: String): Result = simulateLongTask { + setOwnMemberDisplayNameResult(displayName) + } + private suspend fun simulateSendMediaProgress(progressCallback: ProgressCallback?) { progressCallbackValues.forEach { (current, total) -> progressCallback?.onProgress(current, total) diff --git a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/Command.kt b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/Command.kt index 0d9e1e8c726..5df871e361f 100644 --- a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/Command.kt +++ b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/Command.kt @@ -109,7 +109,7 @@ enum class Command( parameters = "", description = R.string.slash_command_description_nick_for_room, isAllowedInThread = false, - isSupported = false, + isSupported = true, ), ROOM_AVATAR( command = "/roomavatar", diff --git a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutor.kt b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutor.kt index ad252cb2240..01bac5825bd 100644 --- a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutor.kt +++ b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutor.kt @@ -47,7 +47,7 @@ class CommandExecutor( is SlashCommand.ChangeAvatar -> changeAvatar() is SlashCommand.ChangeAvatarForRoom -> changeAvatarForRoom() is SlashCommand.ChangeDisplayName -> changeDisplayName(slashCommand) - is SlashCommand.ChangeDisplayNameForRoom -> changeDisplayNameForRoom() + is SlashCommand.ChangeDisplayNameForRoom -> changeDisplayNameForRoom(slashCommand) is SlashCommand.ChangeRoomAvatar -> changeRoomAvatar() is SlashCommand.ChangeRoomName -> changeRoomName(slashCommand) is SlashCommand.ChangeTopic -> changeTopic(slashCommand) @@ -171,8 +171,8 @@ class CommandExecutor( return Result.failure(Exception("Not yet implemented")) } - private fun changeDisplayNameForRoom(): Result { - return Result.failure(Exception("Not yet implemented")) + private suspend fun changeDisplayNameForRoom(slashCommand: SlashCommand.ChangeDisplayNameForRoom): Result { + return joinedRoom.setOwnMemberDisplayName(slashCommand.displayName) } private suspend fun changeDisplayName(slashCommand: SlashCommand.ChangeDisplayName): Result { diff --git a/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutorTest.kt b/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutorTest.kt index 497f45c96f1..800cdecfb3c 100644 --- a/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutorTest.kt +++ b/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutorTest.kt @@ -185,10 +185,18 @@ class CommandExecutorTest { } @Test - fun `change display name for room is not supported`() = runTest { - val sut = createCommandExecutor() - val res = sut.proceedAdmin(SlashCommand.ChangeDisplayNameForRoom(A_USER_NAME)) - assertThat(res.isFailure).isTrue() + fun `change display name for room delegates to joined room`() = runTest { + var capturedDisplayName: String? = null + val joinedRoom = FakeJoinedRoom( + setOwnMemberDisplayNameResult = { displayName -> + capturedDisplayName = displayName + Result.success(Unit) + } + ) + val sut = createCommandExecutor(joinedRoom = joinedRoom) + val res = sut.proceedAdmin(SlashCommand.ChangeDisplayNameForRoom("room nick")) + assertThat(res.isSuccess).isTrue() + assertThat(capturedDisplayName).isEqualTo("room nick") } @Test From e1f968390a35e10f7eb45e28fd10698cb8b0b156 Mon Sep 17 00:00:00 2001 From: bxdxnn <267911624+bxdxnn@users.noreply.github.com> Date: Tue, 26 May 2026 11:56:45 +0300 Subject: [PATCH 2/5] fix unused import --- .../android/libraries/slashcommands/impl/CommandExecutorTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutorTest.kt b/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutorTest.kt index 800cdecfb3c..c0f2ce89c25 100644 --- a/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutorTest.kt +++ b/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutorTest.kt @@ -14,7 +14,6 @@ import io.element.android.libraries.matrix.api.timeline.MsgType import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_USER_ID -import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom From 64b6a1523d05ee8138dc7bb5fdc07051cfcdbfb5 Mon Sep 17 00:00:00 2001 From: bxdxnn <267911624+bxdxnn@users.noreply.github.com> Date: Fri, 29 May 2026 10:56:18 +0000 Subject: [PATCH 3/5] Revert screen recomposition fixes --- .../impl/timeline/TimelinePresenter.kt | 27 ++++--------- .../factories/TimelineItemsFactory.kt | 39 ------------------- 2 files changed, 8 insertions(+), 58 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 3af7326ff51..0b99c45e06d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -51,7 +51,6 @@ import io.element.android.libraries.matrix.api.core.asEventId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState import io.element.android.libraries.matrix.api.room.roomMembers -import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin @@ -255,25 +254,15 @@ class TimelinePresenter( } .launchIn(this) - var previousItems: List? = null combine(timelineController.timelineItems(), room.membersStateFlow) { items, membersState -> - val roomMembers = membersState.roomMembers().orEmpty() - if (previousItems !== items) { - previousItems = items - val parent = analyticsService.getLongRunningTransaction(DisplayFirstTimelineItems) - val transaction = parent?.startChild("timelineItemsFactory.replaceWith", "Processing timeline items") - transaction?.putExtraData(AnalyticsUserData.TIMELINE_ITEM_COUNT, items.count().toString()) - timelineItemsFactory.replaceWith( - timelineItems = items, - roomMembers = roomMembers - ) - transaction?.finish() - } else { - timelineItemsFactory.updateRoomMembers( - timelineItems = items, - roomMembers = roomMembers - ) - } + val parent = analyticsService.getLongRunningTransaction(DisplayFirstTimelineItems) + val transaction = parent?.startChild("timelineItemsFactory.replaceWith", "Processing timeline items") + transaction?.putExtraData(AnalyticsUserData.TIMELINE_ITEM_COUNT, items.count().toString()) + timelineItemsFactory.replaceWith( + timelineItems = items, + roomMembers = membersState.roomMembers().orEmpty() + ) + transaction?.finish() items } .onEach(redactedVoiceMessageManager::onEachMatrixTimelineItem) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt index d1ec5b648a9..dc8bdddc923 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt @@ -72,45 +72,6 @@ class TimelineItemsFactory( } } - /** - * Lightweight update that only refreshes member-derived data (e.g., read receipt display names) - * on cached items without rebuilding the diff cache or creating new items. - * Skips emission if no cached items have member-dependent state. - */ - suspend fun updateRoomMembers( - timelineItems: List, - roomMembers: List, - ) = withContext(dispatchers.computation) { - lock.withLock { - var hasUpdates = false - val updatedStates = ArrayList() - for (index in diffCache.indices().reversed()) { - val cacheItem = diffCache.get(index) - if (cacheItem is TimelineItem.Event && roomMembers.isNotEmpty()) { - val updatedItem = eventItemFactory.update( - timelineItem = cacheItem, - receivedMatrixTimelineItem = timelineItems[index] as MatrixTimelineItem.Event, - roomMembers = roomMembers - ) - diffCache[index] = updatedItem - hasUpdates = true - updatedStates.add(updatedItem) - } else if (cacheItem != null) { - updatedStates.add(cacheItem) - } else { - buildAndCacheItem(timelineItems, index, roomMembers)?.also { timelineItemState -> - updatedStates.add(timelineItemState) - } - } - } - if (hasUpdates) { - val result = timelineItemGrouper.group(updatedStates).toImmutableList() - val filteredResult = filterEmptyDaySeparators(result) - _timelineItems.emit(filteredResult) - } - } - } - private suspend fun buildAndEmitTimelineItemStates( timelineItems: List, roomMembers: List, From 4c1a167e2b96d0b19fe4adc9cabc1a3476df9609 Mon Sep 17 00:00:00 2001 From: bxdxnn <267911624+bxdxnn@users.noreply.github.com> Date: Fri, 29 May 2026 18:18:12 +0000 Subject: [PATCH 4/5] Add HS capability check --- .../slashcommands/impl/DefaultSlashCommandService.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandService.kt b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandService.kt index 6cd8688cad6..8234116bb17 100644 --- a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandService.kt +++ b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandService.kt @@ -99,6 +99,14 @@ class DefaultSlashCommandService( override suspend fun proceedAdmin( slashCommand: SlashCommand.SlashCommandAdmin, ): Result { + if (slashCommand is SlashCommand.ChangeDisplayNameForRoom) { + val canUserChangeDisplayName = withTimeoutOrNull(5.seconds) { + capabilitiesProvider.canChangeDisplayName().getOrNull() + } ?: false + if (!canUserChangeDisplayName) { + return Result.failure(Exception("Changing display name is not allowed")) + } + } return commandExecutor.proceedAdmin( slashCommand = slashCommand, ) From 4b99e5914e8dc7303a3b535c223039b39096d0fd Mon Sep 17 00:00:00 2001 From: bxdxnn <267911624+bxdxnn@users.noreply.github.com> Date: Mon, 1 Jun 2026 11:22:11 +0300 Subject: [PATCH 5/5] Address review --- .../slashcommands/impl/DefaultSlashCommandService.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandService.kt b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandService.kt index 8234116bb17..6cd8688cad6 100644 --- a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandService.kt +++ b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandService.kt @@ -99,14 +99,6 @@ class DefaultSlashCommandService( override suspend fun proceedAdmin( slashCommand: SlashCommand.SlashCommandAdmin, ): Result { - if (slashCommand is SlashCommand.ChangeDisplayNameForRoom) { - val canUserChangeDisplayName = withTimeoutOrNull(5.seconds) { - capabilitiesProvider.canChangeDisplayName().getOrNull() - } ?: false - if (!canUserChangeDisplayName) { - return Result.failure(Exception("Changing display name is not allowed")) - } - } return commandExecutor.proceedAdmin( slashCommand = slashCommand, )