From cd1c054f62fb3f005855d861b898a026af894ccc Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 27 May 2026 11:10:50 -0400 Subject: [PATCH] =?UTF-8?q?feat(android):=20AdminRequests=20=E2=80=94=20sh?= =?UTF-8?q?ow=20requester=20username?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit v2 #18. AdminRequestsScreen was showing only the request's display name, not which user asked for it. Now fetches the admin users list in parallel with the requests list, builds a userId → username map, and renders "Requested by " beneath each row. Falls back to an 8-char userId prefix when the users list fetch fails (e.g., admin without users-list access on some future server-side ACL change). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../minstrel/admin/ui/AdminRequestsScreen.kt | 19 ++++++++++++++- .../admin/ui/AdminRequestsViewModel.kt | 23 +++++++++++++++---- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminRequestsScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminRequestsScreen.kt index 013a3cb9..39fa8942 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminRequestsScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminRequestsScreen.kt @@ -81,6 +81,7 @@ fun AdminRequestsScreen( ) is AdminRequestsUiState.Success -> RequestList( rows = s.rows, + usernames = s.usernames, onApprove = viewModel::approve, onReject = viewModel::reject, ) @@ -92,12 +93,20 @@ fun AdminRequestsScreen( @Composable private fun RequestList( rows: List, + usernames: Map, onApprove: (String) -> Unit, onReject: (String) -> Unit, ) { LazyColumn(modifier = Modifier.fillMaxSize()) { items(items = rows, key = { it.id }) { req -> - AdminRequestRow(req = req, onApprove = { onApprove(req.id) }, onReject = { onReject(req.id) }) + AdminRequestRow( + req = req, + requester = usernames[req.userId] + ?: req.userId.takeIf { it.isNotEmpty() }?.take(8) + ?: "unknown", + onApprove = { onApprove(req.id) }, + onReject = { onReject(req.id) }, + ) HorizontalDivider() } } @@ -106,6 +115,7 @@ private fun RequestList( @Composable private fun AdminRequestRow( req: RequestRef, + requester: String, onApprove: () -> Unit, onReject: () -> Unit, ) { @@ -122,6 +132,13 @@ private fun AdminRequestRow( maxLines = 1, overflow = TextOverflow.Ellipsis, ) + Text( + text = "Requested by $requester", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { AssistChip( onClick = {}, diff --git a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminRequestsViewModel.kt b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminRequestsViewModel.kt index 657722a1..3e3ba216 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminRequestsViewModel.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/admin/ui/AdminRequestsViewModel.kt @@ -3,10 +3,13 @@ package com.fabledsword.minstrel.admin.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.fabledsword.minstrel.admin.data.AdminRequestsRepository +import com.fabledsword.minstrel.admin.data.AdminUsersRepository import com.fabledsword.minstrel.events.EventsStream import com.fabledsword.minstrel.models.RequestRef import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -19,13 +22,17 @@ private val RELEVANT_EVENT_KINDS = setOf("request.status_changed") sealed interface AdminRequestsUiState { data object Loading : AdminRequestsUiState data object Empty : AdminRequestsUiState - data class Success(val rows: List) : AdminRequestsUiState + data class Success( + val rows: List, + val usernames: Map, + ) : AdminRequestsUiState data class Error(val message: String) : AdminRequestsUiState } @HiltViewModel class AdminRequestsViewModel @Inject constructor( private val repository: AdminRequestsRepository, + private val usersRepository: AdminUsersRepository, private val eventsStream: EventsStream, ) : ViewModel() { @@ -44,11 +51,19 @@ class AdminRequestsViewModel @Inject constructor( fun refresh(): Job = viewModelScope.launch { internal.value = AdminRequestsUiState.Loading try { - val rows = repository.list() + val (rows, usernames) = coroutineScope { + val requestsJob = async { repository.list() } + val usernamesJob = async { + runCatching { + usersRepository.list().associate { it.id to it.username } + }.getOrDefault(emptyMap()) + } + requestsJob.await() to usernamesJob.await() + } internal.value = if (rows.isEmpty()) { AdminRequestsUiState.Empty } else { - AdminRequestsUiState.Success(rows) + AdminRequestsUiState.Success(rows = rows, usernames = usernames) } } catch ( @Suppress("TooGenericExceptionCaught") e: Throwable, @@ -75,7 +90,7 @@ class AdminRequestsViewModel @Inject constructor( internal.value = if (filtered.isEmpty()) { AdminRequestsUiState.Empty } else { - AdminRequestsUiState.Success(filtered) + AdminRequestsUiState.Success(rows = filtered, usernames = before.usernames) } } viewModelScope.launch {