feat(android): Material Design UI#26821
Conversation
Migrate chat UI to MaterialTheme values and remove the session connection pill. Replaced legacy color tokens (mobileAccent, mobileWarning, mobileBorder, etc.) and mobile typography with MaterialTheme.colorScheme and MaterialTheme.typography equivalents; updated image Surface border and background to use outlineVariant and surfaceContainerHighest. Removed the healthOk parameter and the ChatConnectionPill composable from ChatSheetContent/ChatThreadSelector and simplified the session label layout accordingly.
Large UI refactor across chat components to modernize and polish the chat experience. Key changes: - ChatComposer: rebuilt input area using BasicTextField, added IME handling (Send action), imePadding, adaptive send/stop/refresh/mic buttons, thinking control block, improved attachments strip placement, and visual refinements (rounded surface, subtle borders, progress indicator). - ChatMessageListCard: added background tint, safer scroll-to-top check, adjusted paddings/spacing and improved empty-chat hint styling for light/dark modes. - ChatMessageViews: simplified and unified message bubble styling (new rounded shapes, spacing, and text color use), removed legacy mobile theme tokens, improved typing indicator, pending tools layout, base64 image display scaling, dot pulse and code block styling. - ChatSheetContent & thread selector: use imePadding, layout spacing and chip styling tweaks, simplified error rail. Also removed/merged several small helper functions and replaced custom tokens with MaterialTheme usages to improve consistency and maintainability.
apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatMessageViews.kt
Outdated
Show resolved
Hide resolved
Adjust ChatComposer right-side controls to show Stop while processing and a combined Refresh + Mic group when the input is empty (instead of separate states). Tweak icon sizes, spacing and colors (mic uses primary tint; refresh reduced to 20dp) to improve layout and affordance. Replace the static PulseDot typing indicator in ChatMessageViews with an AnimatedDots implementation using rememberInfiniteTransition and staggered animateFloat tweens (reverse repeat). Add padding/spacing and required animation imports to provide a Material 3-style animated typing indicator.
Introduce a user-configurable theme mode and switch UI components to use MaterialTheme. Adds SecurePrefsThemeMode enum and persistence (SecurePrefs), exposes a themeMode StateFlow and setter through NodeRuntime and MainViewModel, and MainActivity collects and passes the mode into OpenClawTheme. OpenClawTheme now resolves dark/light based on the selected mode. SettingsSheet gains an Appearance section with radio options (System/Light/Dark). Several UI files (OnboardingFlow, ChatMarkdown, VoiceTabScreen, SettingsSheet) were updated to use MaterialTheme colors/typography and simplified styles/typography, and minor layout/label cleanups were made.
Remove the static "Session" header and the current session label (and its computed currentSessionLabel/friendlySessionName) from ChatThreadSelector. Cleans up an unused variable and simplifies the selector UI while keeping the session options row intact.
Remove the title content from the post-onboarding top app bar and enhance the VoiceTabScreen UX: add tactile haptic feedback when toggling the mic (performHapticFeedback handling API levels and errors), smooth audio level animation (animateFloatAsState), increase and densify waveform bars, adjust animation timing/spacing, and update color thresholds for audio intensity. Also minor imports and layout tweaks to support the changes. These changes improve visual responsiveness and provide tactile confirmation for mic actions.
Break out SettingsSheet into reusable composable sections (SettingsHeader, SectionCard, AppearanceSection, NodeSection, VoiceSection, CameraSection, MessagingSection, LocationSection, ScreenSection, DebugSection) and small building blocks (ThemeOption, PermissionCard, ToggleRow, InfoRow, LocationOption). Replace many ListItem blocks with Material3 cards, icons and compact rows, simplify layout/spacing and typography, and centralize openAppSettings. Also improve VoiceTabScreen mic status rendering: detect error states and show an error-styled surface for error messages (or muted text for normal status) instead of the previous button. Overall UI/UX and code organization improvements for settings and voice status handling.
Pass the activeTab into the top status bar and render its label as the CenterAlignedTopAppBar title. Remove the redundant SettingsHeader composable and its list item from SettingsSheet. Simplify VoiceTabScreen layout by removing the header row (including the Connected badge), the explanatory empty-state text and micStatusText display; adjust LazyColumn modifiers and spacing to match the streamlined layout. Minor import adjustments to support the layout changes.
Replace the horizontally scrollable row of FilterChips with a single FilterChip that opens a DropdownMenu. The chip shows the current session and toggles expand/collapse; the menu lists sessions, highlights the active one with a check icon and stronger typography, and closes on selection. Added state and UI imports to support the dropdown and adjusted styling/colors for selected state to improve usability and reduce horizontal scrolling.
|
Done |
|
Hey, just wanted to bump this in case it got buried — happy to address any feedback or make changes if needed |
Greptile SummaryThis PR successfully migrates the Android app from custom legacy color tokens ( Issues found:
Confidence Score: 3/5
Last reviewed commit: bf58c75 |
| val surfaceColor = MaterialTheme.colorScheme.surface | ||
| val isDark = surfaceColor.red * 0.299 + surfaceColor.green * 0.587 + surfaceColor.blue * 0.114 < 0.5 | ||
| val bgColor = if (isDark) Color(0xFF17212B) else Color(0xFFEFEFF3) |
There was a problem hiding this comment.
Hardcoded colors bypass dynamic theming
The chat list background is computed from a luminance check and then mapped to fixed hex values (0xFF17212B for dark, 0xFFEFEFF3 for light). These are Telegram-inspired colors that won't respond to Material You dynamic color palettes, which is the core goal of this PR.
MaterialTheme.colorScheme.surface (or surfaceContainerLowest) should be used directly instead — it already tracks the user's chosen theme mode and dynamic color seed.
| val surfaceColor = MaterialTheme.colorScheme.surface | |
| val isDark = surfaceColor.red * 0.299 + surfaceColor.green * 0.587 + surfaceColor.blue * 0.114 < 0.5 | |
| val bgColor = if (isDark) Color(0xFF17212B) else Color(0xFFEFEFF3) | |
| val bgColor = MaterialTheme.colorScheme.surfaceContainerLowest |
The isDark derived variable and the same pair of hardcoded colors in EmptyChatHint (lines 98–99) should be updated in the same way.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatMessageListCard.kt
Line: 35-37
Comment:
**Hardcoded colors bypass dynamic theming**
The chat list background is computed from a luminance check and then mapped to fixed hex values (`0xFF17212B` for dark, `0xFFEFEFF3` for light). These are Telegram-inspired colors that won't respond to Material You dynamic color palettes, which is the core goal of this PR.
`MaterialTheme.colorScheme.surface` (or `surfaceContainerLowest`) should be used directly instead — it already tracks the user's chosen theme mode and dynamic color seed.
```suggestion
val bgColor = MaterialTheme.colorScheme.surfaceContainerLowest
```
The `isDark` derived variable and the same pair of hardcoded colors in `EmptyChatHint` (lines 98–99) should be updated in the same way.
How can I resolve this? If you propose a fix, please make it concise.| private val codeBlockBg = Color(0xFF15171B) | ||
| private val codeBlockText = Color(0xFFE8EAEE) |
There was a problem hiding this comment.
Static code-block colors ignore light theme
codeBlockBg (0xFF15171B) and codeBlockText (0xFFE8EAEE) are dark-mode-only values extracted from the old mobileCodeBg/mobileCodeText tokens. In light mode these will still produce a dark background with near-white text — visually tolerable for code, but inconsistent with the rest of the M3 migration and will look wrong on light-themed devices.
These should be @Composable values drawn from the theme. For example:
// Replace file-level constants with composable usage at call site
val inlineStyles = InlineStyles(
inlineCodeBg = MaterialTheme.colorScheme.surfaceContainerHighest,
inlineCodeColor = MaterialTheme.colorScheme.onSurface,
linkColor = MaterialTheme.colorScheme.primary,
)(and thread the code-block colors similarly through ChatCodeBlock)
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatMarkdown.kt
Line: 85-86
Comment:
**Static code-block colors ignore light theme**
`codeBlockBg` (`0xFF15171B`) and `codeBlockText` (`0xFFE8EAEE`) are dark-mode-only values extracted from the old `mobileCodeBg`/`mobileCodeText` tokens. In light mode these will still produce a dark background with near-white text — visually tolerable for code, but inconsistent with the rest of the M3 migration and will look wrong on light-themed devices.
These should be `@Composable` values drawn from the theme. For example:
```kotlin
// Replace file-level constants with composable usage at call site
val inlineStyles = InlineStyles(
inlineCodeBg = MaterialTheme.colorScheme.surfaceContainerHighest,
inlineCodeColor = MaterialTheme.colorScheme.onSurface,
linkColor = MaterialTheme.colorScheme.primary,
)
```
(and thread the code-block colors similarly through `ChatCodeBlock`)
How can I resolve this? If you propose a fix, please make it concise.| IconButton( | ||
| onClick = { }, | ||
| modifier = Modifier.size(36.dp), | ||
| ) { | ||
| Icon( | ||
| Icons.Default.Refresh, | ||
| contentDescription = "Refresh", | ||
| tint = MaterialTheme.colorScheme.onSurfaceVariant, | ||
| modifier = Modifier.size(20.dp), | ||
| ) | ||
| } | ||
| IconButton( | ||
| onClick = { }, | ||
| modifier = Modifier.size(36.dp), | ||
| ) { | ||
| Icon( | ||
| Icons.Default.Mic, | ||
| contentDescription = "Voice", | ||
| tint = MaterialTheme.colorScheme.primary, | ||
| modifier = Modifier.size(22.dp), | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
Mic and Refresh buttons are non-functional
Both IconButtons in the "empty input" state have onClick = { } — they render as fully interactive controls (correct tint, correct size, accessible content descriptions) but do nothing when tapped. Users who tap either button will get no feedback or action.
- The Refresh button should call
onRefresh(which is already a parameter ofChatComposer). - The Mic button presumably needs a new callback to navigate to or activate the Voice tab; if it isn't wired yet it should at minimum be
enabled = falseor replaced with a placeholder until the navigation hook is implemented.
| IconButton( | |
| onClick = { }, | |
| modifier = Modifier.size(36.dp), | |
| ) { | |
| Icon( | |
| Icons.Default.Refresh, | |
| contentDescription = "Refresh", | |
| tint = MaterialTheme.colorScheme.onSurfaceVariant, | |
| modifier = Modifier.size(20.dp), | |
| ) | |
| } | |
| IconButton( | |
| onClick = { }, | |
| modifier = Modifier.size(36.dp), | |
| ) { | |
| Icon( | |
| Icons.Default.Mic, | |
| contentDescription = "Voice", | |
| tint = MaterialTheme.colorScheme.primary, | |
| modifier = Modifier.size(22.dp), | |
| ) | |
| } | |
| } | |
| IconButton( | |
| onClick = onRefresh, | |
| modifier = Modifier.size(36.dp), | |
| ) { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatComposer.kt
Line: 179-201
Comment:
**Mic and Refresh buttons are non-functional**
Both `IconButton`s in the "empty input" state have `onClick = { }` — they render as fully interactive controls (correct tint, correct size, accessible content descriptions) but do nothing when tapped. Users who tap either button will get no feedback or action.
- The **Refresh** button should call `onRefresh` (which is already a parameter of `ChatComposer`).
- The **Mic** button presumably needs a new callback to navigate to or activate the Voice tab; if it isn't wired yet it should at minimum be `enabled = false` or replaced with a placeholder until the navigation hook is implemented.
```suggestion
IconButton(
onClick = onRefresh,
modifier = Modifier.size(36.dp),
) {
```
How can I resolve this? If you propose a fix, please make it concise.| return if (isSystemInDarkTheme()) { | ||
| scheme.surfaceContainerLow.copy(alpha = 0.92f) | ||
| } else { | ||
| scheme.surfaceContainerHigh.copy(alpha = 0.88f) | ||
| } |
There was a problem hiding this comment.
overlayContainerColor ignores user-set theme mode
overlayContainerColor() branches on isSystemInDarkTheme(), but after this PR the app can be forced into dark or light mode independently of the OS setting via SecurePrefsThemeMode. If a user selects "Force Dark" while the OS is in light mode, isSystemInDarkTheme() returns false, so the overlay will use the light treatment even though the rendered color scheme is dark — causing a visual mismatch.
The darkTheme decision that OpenClawTheme already computes should drive this helper instead. One approach is to expose darkTheme through LocalDarkTheme or simply check the current color scheme's lightness:
fun overlayContainerColor(): Color {
val scheme = MaterialTheme.colorScheme
val isDark = scheme.surface.luminance() < 0.5f
return if (isDark) {
scheme.surfaceContainerLow.copy(alpha = 0.92f)
} else {
scheme.surfaceContainerHigh.copy(alpha = 0.88f)
}
}Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/android/app/src/main/java/ai/openclaw/android/ui/OpenClawTheme.kt
Line: 112-116
Comment:
**`overlayContainerColor` ignores user-set theme mode**
`overlayContainerColor()` branches on `isSystemInDarkTheme()`, but after this PR the app can be forced into dark or light mode independently of the OS setting via `SecurePrefsThemeMode`. If a user selects "Force Dark" while the OS is in light mode, `isSystemInDarkTheme()` returns `false`, so the overlay will use the light treatment even though the rendered color scheme is dark — causing a visual mismatch.
The `darkTheme` decision that `OpenClawTheme` already computes should drive this helper instead. One approach is to expose `darkTheme` through `LocalDarkTheme` or simply check the current color scheme's lightness:
```kotlin
fun overlayContainerColor(): Color {
val scheme = MaterialTheme.colorScheme
val isDark = scheme.surface.luminance() < 0.5f
return if (isDark) {
scheme.surfaceContainerLow.copy(alpha = 0.92f)
} else {
scheme.surfaceContainerHigh.copy(alpha = 0.88f)
}
}
```
How can I resolve this? If you propose a fix, please make it concise.| import androidx.compose.foundation.lazy.items | ||
| import androidx.compose.foundation.lazy.items |
There was a problem hiding this comment.
Duplicate import
androidx.compose.foundation.lazy.items is imported twice. Kotlin will emit a warning and some configurations treat duplicate imports as errors. Remove the duplicate.
| import androidx.compose.foundation.lazy.items | |
| import androidx.compose.foundation.lazy.items | |
| import androidx.compose.foundation.lazy.items |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/android/app/src/main/java/ai/openclaw/android/ui/VoiceTabScreen.kt
Line: 46-47
Comment:
**Duplicate import**
`androidx.compose.foundation.lazy.items` is imported twice. Kotlin will emit a warning and some configurations treat duplicate imports as errors. Remove the duplicate.
```suggestion
import androidx.compose.foundation.lazy.items
```
How can I resolve this? If you propose a fix, please make it concise.|
This pull request has been automatically marked as stale due to inactivity. |
|
Closing due to inactivity. |
Pull Request: Android App Redesign (Material Design 3)
Note
AI-Assisted PR: This pull request description and parts of the implementation logic were prepared with the assistance of Gemini 3 Flash, Minimax M2.5
Summary
Change Type
Scope
Changes
Security Impact
No new permissions, secrets, network calls, or data access changes.
Repro + Verification
Environment: Android 13/14, Pixel 7 Emulator
Steps:
Expected: All UI elements follow M3 guidelines, Dynamic Color adjusts on supported devices.
Actual: UI consistent with M3; no visual regressions found.
Screenshots
Human Verification
Compatibility
YesMaterialComponentscompatibility library for graceful fallback on legacy devicesRevert
Revert this PR and restore
themes.xml,colors.xml, and layout resource files. Watch for color contrast issues on older API levels or missing icons in navigation states.Greptile Summary
Successfully migrated Android UI from legacy components to Material Design 3, implementing dynamic color support (Material You), updated color palettes for light/dark themes, and modern M3-compliant components throughout the app.
mobileAccent,mobileText, etc.) withMaterialTheme.colorSchemepropertiesConfidence Score: 4/5
ChatMessageViews.kt:351— hardcoded border color needs theme integration for proper dark mode supportLast reviewed commit: 10eabbc