Add /myroomnick slash command

This commit is contained in:
bxdxnn
2026-05-26 07:36:57 +00:00
parent f447560665
commit 51aa8208e0
8 changed files with 92 additions and 16 deletions
@@ -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<MatrixTimelineItem>? = 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)
@@ -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<MatrixTimelineItem>,
roomMembers: List<RoomMember>,
) = withContext(dispatchers.computation) {
lock.withLock {
var hasUpdates = false
val updatedStates = ArrayList<TimelineItem>()
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<MatrixTimelineItem>,
roomMembers: List<RoomMember>,
@@ -212,4 +212,11 @@ interface JoinedRoom : BaseRoom {
* @return Result indicating success or failure.
*/
suspend fun sendLiveLocation(geoUri: String): Result<Unit>
/**
* 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<Unit>
}
@@ -546,6 +546,12 @@ class JoinedRustRoom(
}
}
override suspend fun setOwnMemberDisplayName(displayName: String): Result<Unit> = withContext(roomDispatcher) {
runCatchingExceptions {
innerRoom.setOwnMemberDisplayName(displayName)
}
}
override fun close() = destroy()
override fun destroy() {
@@ -92,6 +92,7 @@ class FakeJoinedRoom(
private val startLiveLocationShareResult: (Long) -> Result<EventId> = { lambdaError() },
private val stopLiveLocationShareResult: () -> Result<Unit> = { lambdaError() },
private val sendLiveLocationResult: (String) -> Result<Unit> = { lambdaError() },
private val setOwnMemberDisplayNameResult: (String) -> Result<Unit> = { lambdaError() },
) : JoinedRoom, BaseRoom by baseRoom {
private val sendQueueUpdates = MutableSharedFlow<SendQueueUpdate>(extraBufferCapacity = 10)
@@ -255,6 +256,10 @@ class FakeJoinedRoom(
sendLiveLocationResult(geoUri)
}
override suspend fun setOwnMemberDisplayName(displayName: String): Result<Unit> = simulateLongTask {
setOwnMemberDisplayNameResult(displayName)
}
private suspend fun simulateSendMediaProgress(progressCallback: ProgressCallback?) {
progressCallbackValues.forEach { (current, total) ->
progressCallback?.onProgress(current, total)
@@ -109,7 +109,7 @@ enum class Command(
parameters = "<display-name>",
description = R.string.slash_command_description_nick_for_room,
isAllowedInThread = false,
isSupported = false,
isSupported = true,
),
ROOM_AVATAR(
command = "/roomavatar",
@@ -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<Unit> {
return Result.failure(Exception("Not yet implemented"))
private suspend fun changeDisplayNameForRoom(slashCommand: SlashCommand.ChangeDisplayNameForRoom): Result<Unit> {
return joinedRoom.setOwnMemberDisplayName(slashCommand.displayName)
}
private suspend fun changeDisplayName(slashCommand: SlashCommand.ChangeDisplayName): Result<Unit> {
@@ -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