feat: support session-level hidden flag in SessionListResponse and SessionsScreen (#1033)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
aa28d96b5d
commit
908255f5ad
10 files changed
+316
-20
No files matched your search
@@ -46,6 +46,8 @@ import androidx.compose.material.icons.filled.PushPin
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.SelectAll
|
||||
import androidx.compose.material.icons.filled.Terminal
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
@@ -59,6 +61,7 @@ import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
@@ -290,7 +293,7 @@ private fun displayedSessions(state: SessionsUiState): List<SessionTreeItem> =
|
||||
)
|
||||
}
|
||||
} else {
|
||||
flattenSessionTree(state.sessions)
|
||||
flattenSessionTree(state.displaySessions)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@@ -313,6 +316,7 @@ fun SessionsScreen(
|
||||
state.isSearchMode,
|
||||
state.searchQuery,
|
||||
state.sessions,
|
||||
state.showHidden,
|
||||
state.searchResults,
|
||||
) {
|
||||
displayedSessions(state)
|
||||
@@ -521,6 +525,29 @@ fun SessionsScreen(
|
||||
navigationIcon = onOpenDrawer?.let { NavIcon.Menu(it) },
|
||||
isRefreshing = state.isLoading,
|
||||
onRefresh = { viewModel.loadSessions() },
|
||||
actions = {
|
||||
if (state.hasHiddenSessions) {
|
||||
IconButton(
|
||||
onClick = { viewModel.toggleShowHidden() },
|
||||
modifier = Modifier.testTag("sessions_action_toggle_hidden"),
|
||||
) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (state.showHidden) {
|
||||
Icons.Filled.VisibilityOff
|
||||
} else {
|
||||
Icons.Filled.Visibility
|
||||
},
|
||||
contentDescription =
|
||||
if (state.showHidden) {
|
||||
stringResource(R.string.sessions_hide_hidden)
|
||||
} else {
|
||||
stringResource(R.string.sessions_show_hidden)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
) {
|
||||
// Single scrolling column: search bar pinned on top, list fills below.
|
||||
@@ -685,6 +712,16 @@ fun SessionsScreen(
|
||||
)
|
||||
}
|
||||
|
||||
state.displaySessions.isEmpty() -> {
|
||||
EmptyState(
|
||||
title = stringResource(R.string.history_empty_title),
|
||||
subtitle = stringResource(R.string.sessions_all_hidden_desc),
|
||||
icon = Icons.Filled.VisibilityOff,
|
||||
actionLabel = stringResource(R.string.sessions_show_hidden),
|
||||
onAction = { viewModel.toggleShowHidden() },
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// ── Stats row ───────────────────────────────────────
|
||||
@@ -801,6 +838,7 @@ fun SessionsScreen(
|
||||
isSelected = session.id in state.selectedIds,
|
||||
isDeleting = session.id in state.deletingSessionIds,
|
||||
isPinned = session.pinned == true,
|
||||
isHidden = session.hidden == true,
|
||||
highlightBackground = primaryContainer,
|
||||
highlightForeground = onPrimaryContainer,
|
||||
onCardClick = {
|
||||
@@ -822,6 +860,7 @@ fun SessionsScreen(
|
||||
)
|
||||
},
|
||||
onTogglePin = { viewModel.togglePin(session.id) },
|
||||
onToggleHide = { viewModel.toggleHide(session.id) },
|
||||
onDelete = { viewModel.requestDeleteSession(session.id) },
|
||||
)
|
||||
}
|
||||
@@ -969,6 +1008,7 @@ private fun SessionCard(
|
||||
isSelected: Boolean,
|
||||
isDeleting: Boolean,
|
||||
isPinned: Boolean,
|
||||
isHidden: Boolean = false,
|
||||
highlightBackground: Color,
|
||||
highlightForeground: Color,
|
||||
onCardClick: () -> Unit,
|
||||
@@ -976,6 +1016,7 @@ private fun SessionCard(
|
||||
onSelect: () -> Unit,
|
||||
onRename: () -> Unit,
|
||||
onTogglePin: () -> Unit,
|
||||
onToggleHide: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
val spacing = LocalSpacing.current
|
||||
@@ -1095,6 +1136,13 @@ private fun SessionCard(
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
if (session.hidden == true) {
|
||||
Spacer(modifier = Modifier.width(spacing.sm))
|
||||
StatusBadge(
|
||||
text = stringResource(R.string.sessions_hidden_badge),
|
||||
status = StatusBadgeType.NEUTRAL,
|
||||
)
|
||||
}
|
||||
if (!session.status.isNullOrBlank()) {
|
||||
Spacer(modifier = Modifier.width(spacing.sm))
|
||||
StatusBadge(
|
||||
@@ -1116,6 +1164,11 @@ private fun SessionCard(
|
||||
menuExpanded = false
|
||||
onTogglePin()
|
||||
},
|
||||
isHidden = isHidden,
|
||||
onToggleHide = {
|
||||
menuExpanded = false
|
||||
onToggleHide()
|
||||
},
|
||||
onSelect = {
|
||||
menuExpanded = false
|
||||
onSelect()
|
||||
@@ -1301,6 +1354,8 @@ private fun SessionActionMenu(
|
||||
isDeleting: Boolean,
|
||||
isPinned: Boolean = false,
|
||||
onTogglePin: (() -> Unit)? = null,
|
||||
isHidden: Boolean = false,
|
||||
onToggleHide: (() -> Unit)? = null,
|
||||
onSelect: () -> Unit,
|
||||
onRename: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
@@ -1324,6 +1379,33 @@ private fun SessionActionMenu(
|
||||
onClick = onTogglePin,
|
||||
)
|
||||
}
|
||||
if (onToggleHide != null) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
stringResource(
|
||||
if (isHidden) {
|
||||
R.string.sessions_action_unhide
|
||||
} else {
|
||||
R.string.sessions_action_hide
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (isHidden) {
|
||||
Icons.Filled.Visibility
|
||||
} else {
|
||||
Icons.Filled.VisibilityOff
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = onToggleHide,
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.sessions_action_select)) },
|
||||
leadingIcon = { Icon(Icons.Filled.CheckCircle, contentDescription = null) },
|
||||
|
||||
@@ -89,8 +89,15 @@ data class SessionsUiState(
|
||||
val isSearching: Boolean = false,
|
||||
val searchResults: List<SessionSearchResult> = emptyList(),
|
||||
val searchError: String? = null,
|
||||
val showHidden: Boolean = false,
|
||||
) {
|
||||
val isSearchMode: Boolean get() = searchQuery.isNotBlank()
|
||||
|
||||
val hasHiddenSessions: Boolean
|
||||
get() = sessions.any { it.hidden == true }
|
||||
|
||||
val displaySessions: List<SessionInfo>
|
||||
get() = if (showHidden) sessions else sessions.filter { it.hidden != true }
|
||||
}
|
||||
|
||||
class SessionsViewModel :
|
||||
@@ -315,6 +322,10 @@ class SessionsViewModel :
|
||||
|
||||
// ── Bulk selection ───────────────────────────────────────────────────
|
||||
|
||||
fun toggleShowHidden() {
|
||||
_uiState.update { it.copy(showHidden = !it.showHidden) }
|
||||
}
|
||||
|
||||
fun toggleSelecting() {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
@@ -455,6 +466,49 @@ class SessionsViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hide / unhide (issue #1019) ──────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Toggle the hidden flag on a session via PATCH /api/sessions/{id} ({hidden}).
|
||||
*/
|
||||
fun toggleHide(sessionId: String) {
|
||||
val session = _uiState.value.sessions.find { it.id == sessionId } ?: return
|
||||
val targetHidden = session.hidden != true
|
||||
viewModelScope.launch {
|
||||
val result =
|
||||
safeApiCall {
|
||||
ApiClient.hermesApi.setSessionHidden(
|
||||
sessionId = sessionId,
|
||||
body = SessionRenameRequest(hidden = targetHidden),
|
||||
)
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
sessions =
|
||||
it.sessions
|
||||
.map { s -> if (s.id == sessionId) s.copy(hidden = targetHidden) else s }
|
||||
.pinnedFirst(),
|
||||
toastMessage =
|
||||
if (targetHidden) {
|
||||
"Session hidden"
|
||||
} else {
|
||||
"Session unhidden"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(toastMessage = "Hide failed: ${result.error.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Delete (single) ──────────────────────────────────────────────────
|
||||
|
||||
fun requestDeleteSession(sessionId: String) {
|
||||
|
||||
Reference in new issue
Block a user