feat(nav): rename tab labels to canonical order (#5551)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich
2026-05-20 15:57:08 -07:00
committed by GitHub
parent 9d5e20c742
commit ea85b906e8
18 changed files with 675 additions and 35 deletions

View File

@@ -57,6 +57,14 @@ KMP modules have different task names than pure-Android modules. Using the wrong
- **Protos**: `core/proto/` is a read-only git submodule. Never modify proto files.
- **Branches**: Must start with `feat/`, `fix/`, `chore/`, `docs/`, `build/`, `ci/`, `refactor/`, `test/`, `deps/`, or a numeric spec prefix. Always branch off `origin/main`.
<!-- SPECKIT START -->
## Active Plan
- **Feature**: Reorder Bottom Navigation Tab Labels
- **Plan**: `specs/20260520-153412-nav-tab-labels/plan.md`
- **Branch**: `jamesarich/issue-5543-alignment-reorder-bottom-navigation-tab-91d55d`
<!-- SPECKIT END -->
## Deeper Guidance
Consult `.skills/` for detailed playbooks:

View File

@@ -1 +1 @@
{"feature_directory":"specs/20260520-153449-node-list-context-menu"}
{"feature_directory":"specs/20260520-153412-nav-tab-labels"}

View File

@@ -59,7 +59,7 @@ fun MainScreen() {
// from the StateFlow (seeded from persisted prefs) so the initial tab is set in one shot.
val initialTab =
if (viewModel.currentDeviceAddressFlow.value.isNullOrSelectedNone()) {
TopLevelDestination.Connections.route
TopLevelDestination.Connect.route
} else {
NodesRoute.Nodes
}
@@ -82,7 +82,7 @@ fun MainScreen() {
scrollToTopEvents = viewModel.scrollToTopEventFlow,
onHandleDeepLink = viewModel::handleDeepLink,
onNavigateToConnections = {
multiBackstack.navigateTopLevel(TopLevelDestination.Connections.route)
multiBackstack.navigateTopLevel(TopLevelDestination.Connect.route)
},
)
mapGraph(backStack)

View File

@@ -83,15 +83,13 @@ private val CurrentTabSaver =
TopLevelDestination.entries.indexOfFirst { it.route::class == state.value::class }.takeIf { it >= 0 }
},
restore = { ordinal ->
mutableStateOf(
TopLevelDestination.entries.getOrNull(ordinal)?.route ?: TopLevelDestination.Connections.route,
)
mutableStateOf(TopLevelDestination.entries.getOrNull(ordinal)?.route ?: TopLevelDestination.Connect.route)
},
)
/** Remembers a [MultiBackstack] for managing independent tab navigation histories with Navigation 3. */
@Composable
fun rememberMultiBackstack(initialTab: NavKey = TopLevelDestination.Connections.route): MultiBackstack {
fun rememberMultiBackstack(initialTab: NavKey = TopLevelDestination.Connect.route): MultiBackstack {
val stacks = mutableMapOf<NavKey, NavBackStack<NavKey>>()
TopLevelDestination.entries.forEach { dest ->

View File

@@ -20,9 +20,9 @@ import androidx.navigation3.runtime.NavKey
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.bottom_nav_settings
import org.meshtastic.core.resources.connections
import org.meshtastic.core.resources.conversations
import org.meshtastic.core.resources.connect
import org.meshtastic.core.resources.map
import org.meshtastic.core.resources.messages
import org.meshtastic.core.resources.nodes
/**
@@ -32,11 +32,11 @@ import org.meshtastic.core.resources.nodes
* and Desktop navigation shells.
*/
enum class TopLevelDestination(val label: StringResource, val route: Route) {
Conversations(Res.string.conversations, ContactsRoute.Contacts),
Messages(Res.string.messages, ContactsRoute.Contacts),
Nodes(Res.string.nodes, NodesRoute.Nodes),
Map(Res.string.map, MapRoute.Map()),
Settings(Res.string.bottom_nav_settings, SettingsRoute.Settings()),
Connections(Res.string.connections, ConnectionsRoute.Connections),
Connect(Res.string.connect, ConnectionsRoute.Connections),
;
companion object {

View File

@@ -85,21 +85,21 @@ class MultiBackstackTest {
@Test
fun `goBack on root of non-start tab returns to start tab`() {
val startTab = TopLevelDestination.Connections.route
val startTab = TopLevelDestination.Connect.route
val multiBackstack = createMultiBackstack(startTab)
val mapStack = NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Map.route)) }
val connectionsStack = NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Connections.route)) }
val connectStack = NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Connect.route)) }
multiBackstack.backStacks =
mapOf(TopLevelDestination.Map.route to mapStack, TopLevelDestination.Connections.route to connectionsStack)
mapOf(TopLevelDestination.Map.route to mapStack, TopLevelDestination.Connect.route to connectStack)
multiBackstack.navigateTopLevel(TopLevelDestination.Map.route)
assertEquals(TopLevelDestination.Map.route, multiBackstack.currentTabRoute)
multiBackstack.goBack()
assertEquals(TopLevelDestination.Connections.route, multiBackstack.currentTabRoute)
assertEquals(TopLevelDestination.Connect.route, multiBackstack.currentTabRoute)
}
@Test
@@ -120,21 +120,18 @@ class MultiBackstackTest {
@Test
fun `handleDeepLink from different tab switches tab and sets stack`() {
// Start on Connections tab
val startTab = TopLevelDestination.Connections.route
// Start on Connect tab
val startTab = TopLevelDestination.Connect.route
val multiBackstack = createMultiBackstack(startTab)
val connectionsStack = NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Connections.route)) }
val connectStack = NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Connect.route)) }
val nodesStack = NavBackStack<NavKey>().apply { addAll(listOf(TopLevelDestination.Nodes.route)) }
multiBackstack.backStacks =
mapOf(
TopLevelDestination.Connections.route to connectionsStack,
TopLevelDestination.Nodes.route to nodesStack,
)
mapOf(TopLevelDestination.Connect.route to connectStack, TopLevelDestination.Nodes.route to nodesStack)
// Verify we start on Connections
assertEquals(TopLevelDestination.Connections.route, multiBackstack.currentTabRoute)
// Verify we start on Connect
assertEquals(TopLevelDestination.Connect.route, multiBackstack.currentTabRoute)
// Deep-link to a TracerouteMap on the Nodes tab (this is the exact pattern
// MeshtasticAppShell uses for traceroute alert "View on Map")

View File

@@ -150,7 +150,7 @@ private fun handleNavigation(
}
}
TopLevelDestination.Conversations -> {
TopLevelDestination.Messages -> {
val onConversationsList = currentKey is ContactsRoute.Contacts
if (!onConversationsList) {
multiBackstack.navigateTopLevel(destination.route)
@@ -180,7 +180,7 @@ private fun NavigationIconContent(
selectedDevice: String?,
uiViewModel: UIViewModel,
) {
val isConnectionsRoute = destination == TopLevelDestination.Connections
val isConnectionsRoute = destination == TopLevelDestination.Connect
TooltipBox(
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
@@ -211,7 +211,7 @@ private fun NavigationIconContent(
} else {
BadgedBox(
badge = {
if (destination == TopLevelDestination.Conversations) {
if (destination == TopLevelDestination.Messages) {
var lastNonZeroCount by remember { mutableIntStateOf(unreadMessageCount) }
if (unreadMessageCount > 0) {
lastNonZeroCount = unreadMessageCount

View File

@@ -29,9 +29,9 @@ import org.meshtastic.core.resources.ic_wifi
val TopLevelDestination.icon: DrawableResource
get() =
when (this) {
TopLevelDestination.Conversations -> Res.drawable.ic_forum
TopLevelDestination.Messages -> Res.drawable.ic_forum
TopLevelDestination.Nodes -> Res.drawable.ic_nodes
TopLevelDestination.Map -> Res.drawable.ic_map
TopLevelDestination.Settings -> Res.drawable.ic_settings
TopLevelDestination.Connections -> Res.drawable.ic_wifi
TopLevelDestination.Connect -> Res.drawable.ic_wifi
}

View File

@@ -285,7 +285,7 @@ private fun ApplicationScope.MeshtasticWindow(
rememberMultiBackstack(
// Land on Connections for first-run / no-device-selected; otherwise on Nodes.
if (uiViewModel.currentDeviceAddressFlow.value.let { it.isNullOrBlank() || it == "n" }) {
TopLevelDestination.Connections.route
TopLevelDestination.Connect.route
} else {
TopLevelDestination.Nodes.route
},
@@ -360,7 +360,7 @@ private fun handleKeyboardShortcut(
}
Key.One -> {
multiBackstack.navigateTopLevel(TopLevelDestination.Conversations.route)
multiBackstack.navigateTopLevel(TopLevelDestination.Messages.route)
true
}
@@ -375,7 +375,7 @@ private fun handleKeyboardShortcut(
}
Key.Four -> {
multiBackstack.navigateTopLevel(TopLevelDestination.Connections.route)
multiBackstack.navigateTopLevel(TopLevelDestination.Connect.route)
true
}

View File

@@ -47,7 +47,7 @@ fun EntryProviderScope<NavKey>.desktopNavGraph(
backStack = backStack,
scrollToTopEvents = uiViewModel.scrollToTopEventFlow,
onHandleDeepLink = uiViewModel::handleDeepLink,
onNavigateToConnections = { multiBackstack.navigateTopLevel(TopLevelDestination.Connections.route) },
onNavigateToConnections = { multiBackstack.navigateTopLevel(TopLevelDestination.Connect.route) },
)
contactsGraph(backStack, uiViewModel.scrollToTopEventFlow)
mapGraph(backStack)

View File

@@ -30,8 +30,8 @@ import kotlin.test.assertEquals
import kotlin.test.assertFalse
/**
* Keeps Desktop top-level destinations aligned with Android top-level navigation (Conversations, Nodes, Map, Settings,
* Connections).
* Keeps Desktop top-level destinations aligned with Android top-level navigation (Messages, Nodes, Map, Settings,
* Connect).
*/
class DesktopTopLevelDestinationParityTest {

View File

@@ -0,0 +1,36 @@
# Specification Quality Checklist: Reorder Bottom Navigation Tab Labels
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2025-05-20
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- All items pass. Specification is ready for `/speckit.clarify` or `/speckit.plan`.
- This is a straightforward label rename with well-defined scope and clear acceptance criteria.
- No [NEEDS CLARIFICATION] markers were needed — the issue description fully specifies the required changes.

View File

@@ -0,0 +1,60 @@
# Data Model: Nav Tab Labels Rename
**Feature**: Reorder Bottom Navigation Tab Labels
**Date**: 2026-05-20
## Entities
### TopLevelDestination (Enum)
The shared enum defining the canonical set of top-level navigation destinations.
| Entry | Label Resource | Route | Position |
|-------|---------------|-------|----------|
| `Messages` | `Res.string.messages` | `ContactsRoute.Contacts` | 1 |
| `Nodes` | `Res.string.nodes` | `NodesRoute.Nodes` | 2 |
| `Map` | `Res.string.map` | `MapRoute.Map()` | 3 |
| `Settings` | `Res.string.bottom_nav_settings` | `SettingsRoute.Settings()` | 4 |
| `Connect` | `Res.string.connect` | `ConnectionsRoute.Connections` | 5 |
**Changes from current**:
- `Conversations``Messages` (entry rename, label resource changes)
- `Connections``Connect` (entry rename, label resource changes)
### String Resources (New Keys)
| Key | Value (English) | Usage |
|-----|----------------|-------|
| `messages` | `Messages` | Tab label for Messages destination |
| `connect` | `Connect` | Tab label for Connect destination |
### String Resources (Retained — No Changes)
| Key | Value (English) | Usage |
|-----|----------------|-------|
| `conversations` | `Conversations` | Screen title in Contacts.kt |
| `connections` | `Connection` | Screen title in ConnectionsScreen.kt |
## Relationships
```
TopLevelDestination.Messages
├── label → Res.string.messages (NEW)
├── route → ContactsRoute.Contacts (UNCHANGED)
└── icon → Res.drawable.ic_forum (UNCHANGED, via TopLevelDestinationExt)
TopLevelDestination.Connect
├── label → Res.string.connect (NEW)
├── route → ConnectionsRoute.Connections (UNCHANGED)
└── icon → Res.drawable.ic_wifi (UNCHANGED, via TopLevelDestinationExt)
```
## State Transitions
N/A — No state machines affected. The enum is purely declarative.
## Validation Rules
- Tab order MUST remain: Messages (0), Nodes (1), Map (2), Settings (3), Connect (4)
- Enum ordinal positions MUST NOT change (preserves `MultiBackstack` ordinal-based fallback)
- Route objects MUST NOT change (preserves navigation, deep links, state restoration)

View File

@@ -0,0 +1,79 @@
# Implementation Plan: Reorder Bottom Navigation Tab Labels
**Branch**: `jamesarich/issue-5543-alignment-reorder-bottom-navigation-tab-91d55d` | **Date**: 2026-05-20 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `specs/20260520-153412-nav-tab-labels/spec.md`
## Summary
Rename two bottom navigation tab labels from "Conversations" → "Messages" and "Connections" → "Connect" to match the cross-platform canonical naming convention from the Menu Alignment Audit. Implementation involves: renaming the `TopLevelDestination` enum entries, adding new string resource keys for tab labels, and updating all references across the KMP codebase. Existing string keys are retained for screen titles.
## Technical Context
**Language/Version**: Kotlin 2.3+ / JDK 21
**Primary Dependencies**: Compose Multiplatform, Navigation 3, Koin 4.2+
**Storage**: N/A (string resource change only)
**Testing**: `./gradlew allTests` (KMP commonTest), `./gradlew test` (Android-only)
**Target Platform**: Android (mobile), Desktop (JVM) — KMP shared code
**Project Type**: Mobile app (KMP multiplatform)
**Performance Goals**: N/A (label change only, no runtime impact)
**Constraints**: Must not break navigation routing, deep links, or state restoration
**Scale/Scope**: 7 files modified across 3 modules (`core:navigation`, `core:ui`, `core:resources`) + test updates
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- **I. Kotlin Multiplatform Core**: ✅ All changes are in `commonMain` source sets (`core/navigation`, `core/ui`, `core/resources`). No platform-specific (`androidMain`/`desktopMain`) code is modified. Enum rename and string resources are shared across all targets.
- **II. Zero Lint Tolerance**: ✅ Will run: `./gradlew spotlessApply spotlessCheck detekt` for all touched modules. After adding string resources: `python3 scripts/sort-strings.py`.
- **III. Compose Multiplatform UI**: ✅ No new UI composables introduced. The `MeshtasticNavigationSuite` already uses `TopLevelDestination.label` via `stringResource()`; the label change is purely data-driven. No float formatting involved.
- **IV. Privacy First**: ✅ No PII, location data, or cryptographic keys involved. `core/proto` submodule is not touched.
- **V. Design Standards Compliance**: ✅ This change directly implements the [Menu Alignment Audit](https://github.com/meshtastic/design/blob/master/standards/audits/menu-alignment-audit.md) from `meshtastic/design`. Cross-platform behavior spec is the audit itself.
- **VI. Documentation Freshness**: ✅ No doc pages require updates — this is a label rename with no feature behavior change. Existing docs reference screen functionality, not tab label text.
- **VII. Verify Before Push**: Will run:
```bash
./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests
python3 scripts/sort-strings.py
```
Post-push: `gh pr checks <PR>` or `gh run list --branch jamesarich/issue-5543-alignment-reorder-bottom-navigation-tab-91d55d --limit 5`
## Project Structure
### Documentation (this feature)
```text
specs/20260520-153412-nav-tab-labels/
├── plan.md # This file
├── research.md # Phase 0 output
├── data-model.md # Phase 1 output
├── quickstart.md # Phase 1 output
└── tasks.md # Phase 2 output (via /speckit.tasks)
```
### Source Code (repository root)
```text
core/
├── navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/
│ ├── TopLevelDestination.kt # Enum entries renamed
│ └── MultiBackstack.kt # References updated
├── navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/
│ └── MultiBackstackTest.kt # Test references updated
├── ui/src/commonMain/kotlin/org/meshtastic/core/ui/
│ ├── navigation/TopLevelDestinationExt.kt # Icon mapping updated
│ └── component/MeshtasticNavigationSuite.kt # References updated
├── resources/src/commonMain/composeResources/values/
│ └── strings.xml # New keys: messages, connect
androidApp/src/main/kotlin/org/meshtastic/app/ui/
│ └── Main.kt # References updated
desktopApp/src/main/kotlin/org/meshtastic/desktop/
│ ├── Main.kt # References updated
│ └── navigation/DesktopNavigation.kt # References updated
desktopApp/src/test/kotlin/org/meshtastic/desktop/ui/
│ └── DesktopTopLevelDestinationParityTest.kt # May need update
```
**Structure Decision**: Kotlin Multiplatform with shared `commonMain` source sets. All changes are in existing files; no new modules or directories created.
## Complexity Tracking
> No constitution violations. All gates pass.

View File

@@ -0,0 +1,63 @@
# Quickstart: Nav Tab Labels Rename
**Feature**: Reorder Bottom Navigation Tab Labels
**Date**: 2026-05-20
## Overview
This feature renames two `TopLevelDestination` enum entries and their associated string resources to align with the Meshtastic cross-platform Menu Alignment Audit.
## Implementation Steps (High-Level)
### Step 1: Add String Resources
Add two new keys to `core/resources/src/commonMain/composeResources/values/strings.xml`:
```xml
<string name="connect">Connect</string>
<string name="messages">Messages</string>
```
Then run: `python3 scripts/sort-strings.py`
### Step 2: Rename Enum Entries
In `core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/TopLevelDestination.kt`:
- `Conversations(Res.string.conversations, ...)``Messages(Res.string.messages, ...)`
- `Connections(Res.string.connections, ...)``Connect(Res.string.connect, ...)`
Update imports accordingly.
### Step 3: Update All References
Files requiring mechanical rename of `TopLevelDestination.Conversations``.Messages` and `.Connections``.Connect`:
1. `core/ui/.../TopLevelDestinationExt.kt` — icon `when` branches
2. `core/ui/.../MeshtasticNavigationSuite.kt` — any explicit references
3. `core/navigation/.../MultiBackstack.kt` — default tab reference
4. `core/navigation/src/commonTest/.../MultiBackstackTest.kt` — test setup
5. `androidApp/.../Main.kt` — Android entry point
6. `desktopApp/.../Main.kt` — Desktop entry point
7. `desktopApp/.../DesktopNavigation.kt` — Desktop nav shell
### Step 4: Verify
```bash
./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests
```
## Key Decisions
| Decision | Rationale |
|----------|-----------|
| New string keys (not reusing old) | Old keys used as screen titles elsewhere |
| Enum entry rename (not just label) | Code clarity + spec requirement |
| No route changes | Preserves deep links & state restoration |
| No localization changes | Deferred to Crowdin sync cycle |
## Risks & Mitigations
| Risk | Mitigation |
|------|-----------|
| Missed reference causes compile error | IDE rename refactoring + full `allTests` build |
| Ordinal shift breaks MultiBackstack | Entries stay in same position — order unchanged |
| Localized users see English fallback | Expected behavior for new keys until Crowdin sync |

View File

@@ -0,0 +1,78 @@
# Research: Nav Tab Labels Rename
**Feature**: Reorder Bottom Navigation Tab Labels
**Date**: 2026-05-20
## Research Tasks
### 1. Enum Rename Impact on Serialization / State Restoration
**Decision**: Renaming `TopLevelDestination.Conversations``Messages` and `TopLevelDestination.Connections``Connect` is safe for navigation state.
**Rationale**:
- Navigation state is keyed by `NavKey` route objects (e.g., `ContactsRoute.Contacts`, `ConnectionsRoute.Connections`), NOT by enum entry names.
- The `TopLevelDestination.route` property maps to typed route objects — the enum name is never serialized.
- `MultiBackstack` stores `NavKey` instances in its back stack maps, keyed by route class identity (`it::class == dest.route::class`).
- Process death restoration uses `SavedStateHandle` with route objects, not enum string names.
**Alternatives Considered**:
- Keep old enum names and only change labels: Rejected because the spec explicitly requires renaming entries for code clarity and alignment with the canonical naming.
### 2. String Resource Key Strategy
**Decision**: Add new keys `messages` and `connect` for tab labels. Retain existing keys `conversations` and `connections` for screen titles.
**Rationale**:
- `conversations` is used in `Contacts.kt` for the screen title (confirmed from spec clarification session).
- `connections` is used in `ConnectionsScreen.kt` for the screen/section title.
- Separating tab label keys from screen title keys allows independent localization and prevents unintended label changes elsewhere.
- The `connections` key currently has value "Connection" (singular) — this is a screen title, not a tab label.
**Alternatives Considered**:
- Reuse existing keys and rename their values: Rejected because it would change screen titles in feature modules that are out of scope.
- Use `tab_messages` / `tab_connect` prefixed keys: Rejected in favor of simpler `messages` / `connect` per clarification decision.
### 3. Compose Resource Import Changes
**Decision**: After adding `messages` and `connect` to `strings.xml`, the generated `Res.string.messages` and `Res.string.connect` accessors will be available after a build.
**Rationale**:
- Compose Multiplatform generates accessor objects from resource keys at compile time.
- Import statements in `TopLevelDestination.kt` will change from `org.meshtastic.core.resources.conversations` / `org.meshtastic.core.resources.connections` to `org.meshtastic.core.resources.messages` / `org.meshtastic.core.resources.connect`.
- Old imports (`conversations`, `connections`) remain valid since those keys are retained — they just won't be used in `TopLevelDestination.kt` anymore.
**Alternatives Considered**: None — this is the standard Compose Resources workflow.
### 4. Test Impact Assessment
**Decision**: Update `MultiBackstackTest.kt` references from `TopLevelDestination.Connections` to `TopLevelDestination.Connect`. The `DesktopTopLevelDestinationParityTest.kt` tests enum parity generically (iterates `entries`) and requires no changes.
**Rationale**:
- `MultiBackstackTest.kt` explicitly references `TopLevelDestination.Connections` in 8 locations for default tab setup.
- The parity test uses `TopLevelDestination.entries` enumeration, so it adapts automatically to renamed entries.
- No test logic changes needed — only identifier renames.
**Alternatives Considered**: None — mechanical rename with no behavioral changes.
### 5. Sort Script for String Resources
**Decision**: Run `python3 scripts/sort-strings.py` after adding new keys to maintain alphabetical ordering.
**Rationale**:
- Constitution (Development Workflow) mandates running this script after any string resource addition.
- Keys `connect` and `messages` will be inserted alphabetically by the script.
- `strings-index.txt` will be regenerated automatically.
**Alternatives Considered**: None — this is a mandatory workflow step.
## Summary of Resolved Items
| Item | Resolution |
|------|-----------|
| Enum rename breaks state? | No — routes use typed objects, not enum names |
| String key strategy | New keys `messages`/`connect`; old keys retained |
| Import changes | Mechanical — generated accessors update on build |
| Test updates needed | `MultiBackstackTest.kt` identifier renames only |
| Post-change workflow | `python3 scripts/sort-strings.py` required |
All NEEDS CLARIFICATION items resolved. No open questions remain.

View File

@@ -0,0 +1,157 @@
# Feature Specification: Reorder Bottom Navigation Tab Labels
**Feature Branch**: `jamesarich/issue-5543-alignment-reorder-bottom-navigation-tab-91d55d`
**Created**: 2025-05-20
**Status**: Draft
**Input**: User description: "Issue #5543 - Reorder bottom navigation tabs to canonical order per Menu Alignment Audit"
**Cross-Platform Spec**: [Menu Alignment Audit](https://github.com/meshtastic/design/blob/master/standards/audits/menu-alignment-audit.md)
## Clarifications
### Session 2026-05-20
- Q: Should string resource keys be renamed (not just display values)? → A: Yes — create new keys `messages` and `connect` for nav tab labels. Old keys (`conversations`, `connections`) remain for screen titles in Contacts.kt and ConnectionsScreen.kt.
- Q: Should localized strings.xml files be updated in this PR? → A: No — defer to Crowdin. Only update English `values/strings.xml`; localized files pick up new keys in next translation sync.
## Summary
Rename two bottom navigation tab labels to match the cross-platform canonical naming convention defined in the Meshtastic Menu Alignment Audit. "Conversations" becomes "Messages" and "Connections" becomes "Connect". Tab order already matches the canonical order and requires no positional changes.
## Goals
1. Achieve cross-platform label consistency by aligning Android tab names with the canonical standard
2. Improve user familiarity by using industry-standard terminology ("Messages" for messaging)
3. Reduce cognitive friction for users switching between Meshtastic clients on different platforms
4. Maintain existing navigation behavior and tab ordering without regressions
## Non-Goals
- Changing the tab order (already matches canonical: Messages, Nodes, Map, Settings, Connect)
- Redesigning tab icons or visual styling
- Adding, removing, or merging navigation tabs
- Changing functionality behind any tab
- Modifying navigation deep link route identifiers (only user-visible labels change)
## User Scenarios & Testing *(mandatory)*
### User Story 1 - See Updated Tab Labels (Priority: P1)
As a Meshtastic Android user, I see "Messages" and "Connect" as tab labels so that the interface matches documentation and other platform clients.
**Why this priority**: This is the core deliverable — the visual label change that achieves cross-platform alignment.
**Independent Test**: Open the app after update; visually confirm bottom navigation bar shows "Messages" (position 1) and "Connect" (position 5).
**Acceptance Scenarios**:
1. **Given** the app is launched, **When** the bottom navigation bar is visible, **Then** the first tab reads "Messages" (not "Conversations")
2. **Given** the app is launched, **When** the bottom navigation bar is visible, **Then** the fifth tab reads "Connect" (not "Connections")
3. **Given** the app is launched, **When** the bottom navigation bar is visible, **Then** tab order is: Messages, Nodes, Map, Settings, Connect
---
### User Story 2 - Navigation Still Functions After Rename (Priority: P1)
As a user, I can tap the renamed tabs and navigate to the correct screens without any change in behavior.
**Why this priority**: Equal to P1 because broken navigation would be a blocking regression.
**Independent Test**: Tap "Messages" tab → messaging screen loads; tap "Connect" tab → connection/pairing screen loads.
**Acceptance Scenarios**:
1. **Given** the user is on any screen, **When** they tap "Messages", **Then** the messaging/conversations screen appears
2. **Given** the user is on any screen, **When** they tap "Connect", **Then** the device connection/pairing screen appears
3. **Given** the user taps between all five tabs in rapid succession, **When** each tab is selected, **Then** the correct corresponding screen displays without delay or error
---
### User Story 3 - Deep Links and State Restoration Work (Priority: P2)
As a user who receives a notification or restores the app from background, deep links and saved navigation state continue to route correctly.
**Why this priority**: Ensures no regression in system-level navigation behavior that relies on route identifiers.
**Independent Test**: Trigger a message notification → tap it → app opens to the messaging screen. Kill and restore the app → previously selected tab is restored.
**Acceptance Scenarios**:
1. **Given** the app is in the background and a message notification arrives, **When** the user taps the notification, **Then** the app navigates to the messaging screen (now labeled "Messages")
2. **Given** the user is on the "Connect" tab and the system kills the app, **When** the app is restored, **Then** the "Connect" tab is selected and the connection screen is displayed
3. **Given** a deep link targets the messaging section, **When** the link is activated, **Then** the app navigates to the messaging screen under the "Messages" tab
---
### Edge Cases
- What happens if a user has the old version cached and updates? Label change takes effect immediately as it is a string resource change.
- How does the app behave with localized strings? Localized translations for these labels should also be updated for all supported languages.
- What about accessibility services (TalkBack)? The new labels ("Messages", "Connect") are announced correctly by screen readers since they derive from the same string resources.
## Architecture
### Key Components
| Component | Module / File | Purpose |
|-----------|---------------|---------|
| Navigation bar strings | `core/resources/src/commonMain/composeResources/values/strings.xml` | New keys `messages` and `connect` for tab labels; old keys retained for screen titles |
| Localized strings | `core/resources/src/commonMain/composeResources/values-*/strings.xml` | Deferred — new keys picked up in next Crowdin sync |
| Bottom navigation composable | Navigation component referencing string resources | Displays tab labels in bottom bar |
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: The bottom navigation tab currently labeled "Conversations" MUST display "Messages" in English locale
- **FR-002**: The bottom navigation tab currently labeled "Connections" (or "Connection") MUST display "Connect" in English locale
- **FR-003**: Tab order MUST remain: Messages, Nodes, Map, Settings, Connect (positions 15)
- **FR-004**: Tapping any renamed tab MUST navigate to the same destination screen as before the rename
- **FR-005**: Deep links that previously routed to the Conversations or Connections screens MUST continue to function
- **FR-006**: State restoration MUST correctly restore the selected tab after process death
### Non-Functional Requirements
- **NFR-001**: Accessibility — screen readers MUST announce the updated labels ("Messages", "Connect") when tabs receive focus
- **NFR-002**: Localization — translated string values are deferred to next Crowdin sync; only English `values/strings.xml` is updated in this change
- **NFR-003**: No user-perceivable performance impact from the label change
## Source-Set Impact
| Source Set | Impact | Justification |
|-----------|--------|---------------|
| `commonMain` | Modified string resources | Tab labels are defined in shared compose resources |
| `androidMain` | None | No platform-specific changes needed |
| `jvmMain` | None | No desktop-specific changes needed |
## Design Standards Compliance
- [x] New screens reviewed against design standards — No new screens; existing screens unchanged
- [x] M3 component selection verified — No component changes
- [ ] Accessibility: TalkBack semantics, touch targets, color-independent info — Verify renamed labels are announced correctly
- [x] Typography: No typography changes
## Privacy Assessment
- [x] No PII, location data, or cryptographic keys logged or exposed
- [x] No new network calls that transmit user data
- [x] Proto submodule (`core/proto`) not modified (read-only upstream)
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: 100% of users see "Messages" as the first tab label after updating the app
- **SC-002**: 100% of users see "Connect" as the fifth tab label after updating the app
- **SC-003**: Tab order matches canonical sequence (Messages, Nodes, Map, Settings, Connect) across all locales
- **SC-004**: Zero navigation regressions — all existing deep links and state restoration paths continue to function
- **SC-005**: Cross-platform label parity achieved — Android tab names match the Menu Alignment Audit specification
## Assumptions
- All business logic and UI composables reside in `commonMain` source set
- String resources are defined in `core/resources/src/commonMain/composeResources/values/strings.xml`
- New string resource keys (`messages`, `connect`) will be created for bottom navigation tab labels; existing keys (`conversations`, `connections`) are retained for screen titles in feature modules (Contacts.kt, ConnectionsScreen.kt)
- Tab order is already correct (positions 15 match canonical) and no reordering logic changes are needed
- Navigation route identifiers are independent of user-visible label strings (routes use programmatic keys, not display text)
- Localized translations are deferred to the next Crowdin translation sync cycle (not in-scope for this PR)
- The `doc_title_connections` string resource (used elsewhere, e.g., documentation titles) may need separate evaluation but is out of scope for this tab label change

View File

@@ -0,0 +1,164 @@
# Tasks: Reorder Bottom Navigation Tab Labels
**Input**: Design documents from `specs/20260520-153412-nav-tab-labels/`
**Prerequisites**: plan.md ✅, spec.md ✅, research.md ✅, data-model.md ✅, quickstart.md ✅
**Tests**: No new automated tests requested in the feature specification. Existing tests will be updated to reflect the rename but no new test coverage is added.
**Verification**: Constitution-required validation tasks are included for formatting, static analysis, and compile/test commands.
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
- Include exact file paths in descriptions
---
## Phase 1: Setup (String Resources)
**Purpose**: Add new string resource keys required by all subsequent tasks
- [X] T001 Add `messages` and `connect` string keys to `core/resources/src/commonMain/composeResources/values/strings.xml`
- [X] T002 Run `python3 scripts/sort-strings.py` to maintain alphabetical ordering and regenerate strings-index.txt
---
## Phase 2: Foundational (Enum Rename)
**Purpose**: Rename the `TopLevelDestination` enum entries — this BLOCKS all reference updates
**⚠️ CRITICAL**: No reference update tasks can begin until this phase is complete
- [X] T003 Rename `Conversations``Messages` and `Connections``Connect` in `core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/TopLevelDestination.kt` — update label resource references from `Res.string.conversations``Res.string.messages` and `Res.string.connections``Res.string.connect`
**Checkpoint**: Enum entries renamed — reference updates can now proceed in parallel
---
## Phase 3: User Story 1 — See Updated Tab Labels (Priority: P1) 🎯 MVP
**Goal**: Bottom navigation bar displays "Messages" (position 1) and "Connect" (position 5) as tab labels
**Independent Test**: Launch the app → visually confirm bottom navigation shows "Messages" and "Connect" in correct positions
### Implementation for User Story 1
- [X] T004 [P] [US1] Update icon `when` branches for `Messages` and `Connect` in `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/navigation/TopLevelDestinationExt.kt`
- [X] T005 [P] [US1] Update any explicit references from `Conversations`/`Connections` to `Messages`/`Connect` in `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt`
- [X] T006 [P] [US1] Update default tab reference from `Connections` to `Connect` in `core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/MultiBackstack.kt`
- [X] T007 [P] [US1] Update references in `androidApp/src/main/kotlin/org/meshtastic/app/ui/Main.kt`
- [X] T008 [P] [US1] Update references in `desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt`
- [X] T009 [P] [US1] Update references in `desktopApp/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt`
**Checkpoint**: Tab labels display correctly — User Story 1 is visually complete
---
## Phase 4: User Story 2 — Navigation Still Functions After Rename (Priority: P1)
**Goal**: Tapping "Messages" and "Connect" tabs navigates to the correct screens without behavior change
**Independent Test**: Tap "Messages" tab → messaging screen loads; tap "Connect" tab → connection/pairing screen loads; rapid tab switching works without errors
### Implementation for User Story 2
- [X] T010 [US2] Update test references from `TopLevelDestination.Connections` to `TopLevelDestination.Connect` (8 locations) in `core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/MultiBackstackTest.kt`
**Checkpoint**: Navigation functions correctly with renamed enum entries — no behavioral regression
---
## Phase 5: User Story 3 — Deep Links and State Restoration Work (Priority: P2)
**Goal**: Deep links and saved navigation state continue to route correctly after the rename
**Independent Test**: Trigger a message notification → tap it → app opens to messaging screen. Kill and restore app → previously selected tab is restored.
### Implementation for User Story 3
No additional implementation tasks required. Deep links and state restoration use typed route objects (`ContactsRoute.Contacts`, `ConnectionsRoute.Connections`) which are NOT affected by the enum entry rename. This was confirmed in research.md (Research Task 1).
**Checkpoint**: Verified by existing tests — no code changes needed for this story
---
## Phase 6: Polish & Cross-Cutting Concerns
**Purpose**: Verification and constitution-required validation
- [X] T011 [P] Run `./gradlew spotlessApply` to auto-format all touched files
- [X] T012 [P] Run `./gradlew spotlessCheck detekt` to confirm zero lint violations
- [X] T013 Run `./gradlew assembleDebug` to verify project compiles successfully
- [X] T014 Run `./gradlew test allTests` to verify all existing tests pass with renamed references
- [X] T015 Confirm no logs, telemetry, or config changes expose PII, location data, secrets, or modify `core/proto`
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies — can start immediately
- **Foundational (Phase 2)**: Depends on Phase 1 (string resources must exist before enum references them)
- **User Story 1 (Phase 3)**: Depends on Phase 2 (enum must be renamed before updating references)
- **User Story 2 (Phase 4)**: Depends on Phase 2 (enum must be renamed before updating test references)
- **User Story 3 (Phase 5)**: No implementation needed — verified by Phase 4 tests passing
- **Polish (Phase 6)**: Depends on Phases 3 and 4 completion
### User Story Dependencies
- **User Story 1 (P1)**: Can start after Foundational (Phase 2) — No dependencies on other stories
- **User Story 2 (P1)**: Can start after Foundational (Phase 2) — Independent of US1 (different files)
- **User Story 3 (P2)**: Zero implementation — verified by existing route-based navigation tests
### Parallel Opportunities
- **Phase 3**: ALL tasks T004T009 can run in parallel (each touches a different file)
- **Phase 4**: T010 can run in parallel with Phase 3 tasks (different file: test vs production)
- **Phase 6**: T011 and T012 can run in parallel with T015
---
## Parallel Example: User Stories 1 & 2
```bash
# After Phase 2 completes, launch ALL of these in parallel:
Task T004: "Update TopLevelDestinationExt.kt icon branches"
Task T005: "Update MeshtasticNavigationSuite.kt references"
Task T006: "Update MultiBackstack.kt default tab reference"
Task T007: "Update androidApp Main.kt references"
Task T008: "Update desktopApp Main.kt references"
Task T009: "Update DesktopNavigation.kt references"
Task T010: "Update MultiBackstackTest.kt test references"
```
---
## Implementation Strategy
### MVP First (User Stories 1 & 2)
1. Complete Phase 1: Add string resources (T001T002)
2. Complete Phase 2: Rename enum entries (T003)
3. Complete Phase 3: Update all production references (T004T009) — **in parallel**
4. Complete Phase 4: Update test references (T010) — **parallel with Phase 3**
5. **STOP and VALIDATE**: Run `./gradlew assembleDebug test allTests`
6. Complete Phase 6: Polish and verification (T011T015)
### Total Effort Estimate
- **Sequential execution**: 15 tasks, ~30 minutes (mechanical renames)
- **Parallel execution**: Critical path is 6 steps (T001 → T002 → T003 → T004‖T010 → T013 → T014)
- **Risk**: Low — all changes are mechanical identifier renames with no logic changes
---
## Notes
- [P] tasks = different files, no dependencies
- [Story] label maps task to specific user story for traceability
- User Story 3 requires zero implementation — validated by passing tests from US2
- All Phase 3 tasks are mechanical `Conversations``Messages` and `Connections``Connect` identifier replacements
- Commit after Phase 2 and after Phase 3+4 combined for clean git history
- Old string keys (`conversations`, `connections`) are deliberately RETAINED — they serve as screen titles in other modules