From ea85b906e86c00261ec91f9ad7cf4a601417d135 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Wed, 20 May 2026 15:57:08 -0700 Subject: [PATCH] feat(nav): rename tab labels to canonical order (#5551) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 8 + .specify/feature.json | 2 +- .../main/kotlin/org/meshtastic/app/ui/Main.kt | 4 +- .../core/navigation/MultiBackstack.kt | 6 +- .../core/navigation/TopLevelDestination.kt | 8 +- .../core/navigation/MultiBackstackTest.kt | 23 ++- .../ui/component/MeshtasticNavigationSuite.kt | 6 +- .../ui/navigation/TopLevelDestinationExt.kt | 4 +- .../kotlin/org/meshtastic/desktop/Main.kt | 6 +- .../desktop/navigation/DesktopNavigation.kt | 2 +- .../DesktopTopLevelDestinationParityTest.kt | 4 +- .../checklists/requirements.md | 36 ++++ .../data-model.md | 60 +++++++ specs/20260520-153412-nav-tab-labels/plan.md | 79 +++++++++ .../quickstart.md | 63 +++++++ .../research.md | 78 +++++++++ specs/20260520-153412-nav-tab-labels/spec.md | 157 +++++++++++++++++ specs/20260520-153412-nav-tab-labels/tasks.md | 164 ++++++++++++++++++ 18 files changed, 675 insertions(+), 35 deletions(-) create mode 100644 specs/20260520-153412-nav-tab-labels/checklists/requirements.md create mode 100644 specs/20260520-153412-nav-tab-labels/data-model.md create mode 100644 specs/20260520-153412-nav-tab-labels/plan.md create mode 100644 specs/20260520-153412-nav-tab-labels/quickstart.md create mode 100644 specs/20260520-153412-nav-tab-labels/research.md create mode 100644 specs/20260520-153412-nav-tab-labels/spec.md create mode 100644 specs/20260520-153412-nav-tab-labels/tasks.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d357d0d98..0d5c63353 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -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`. + +## 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` + + ## Deeper Guidance Consult `.skills/` for detailed playbooks: diff --git a/.specify/feature.json b/.specify/feature.json index 182006984..cd2b73f68 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1 +1 @@ -{"feature_directory":"specs/20260520-153449-node-list-context-menu"} +{"feature_directory":"specs/20260520-153412-nav-tab-labels"} diff --git a/androidApp/src/main/kotlin/org/meshtastic/app/ui/Main.kt b/androidApp/src/main/kotlin/org/meshtastic/app/ui/Main.kt index ec8dab03e..b27b300df 100644 --- a/androidApp/src/main/kotlin/org/meshtastic/app/ui/Main.kt +++ b/androidApp/src/main/kotlin/org/meshtastic/app/ui/Main.kt @@ -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) diff --git a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/MultiBackstack.kt b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/MultiBackstack.kt index 49f1f55a6..ac977c52c 100644 --- a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/MultiBackstack.kt +++ b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/MultiBackstack.kt @@ -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>() TopLevelDestination.entries.forEach { dest -> diff --git a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/TopLevelDestination.kt b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/TopLevelDestination.kt index 635ecfb7f..27250c75a 100644 --- a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/TopLevelDestination.kt +++ b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/TopLevelDestination.kt @@ -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 { diff --git a/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/MultiBackstackTest.kt b/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/MultiBackstackTest.kt index 4ec1f7139..ebbb5cfd4 100644 --- a/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/MultiBackstackTest.kt +++ b/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/MultiBackstackTest.kt @@ -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().apply { addAll(listOf(TopLevelDestination.Map.route)) } - val connectionsStack = NavBackStack().apply { addAll(listOf(TopLevelDestination.Connections.route)) } + val connectStack = NavBackStack().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().apply { addAll(listOf(TopLevelDestination.Connections.route)) } + val connectStack = NavBackStack().apply { addAll(listOf(TopLevelDestination.Connect.route)) } val nodesStack = NavBackStack().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") diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt index 5fa55b040..2f86e1efe 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticNavigationSuite.kt @@ -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 diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/navigation/TopLevelDestinationExt.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/navigation/TopLevelDestinationExt.kt index dacc6ad48..640b4ba87 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/navigation/TopLevelDestinationExt.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/navigation/TopLevelDestinationExt.kt @@ -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 } diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt index 1d0e0190e..f8ad96508 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt @@ -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 } diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt index 83494ce00..6103ffdbf 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt @@ -47,7 +47,7 @@ fun EntryProviderScope.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) diff --git a/desktopApp/src/test/kotlin/org/meshtastic/desktop/ui/DesktopTopLevelDestinationParityTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/ui/DesktopTopLevelDestinationParityTest.kt index ac81053ef..1e8ffed2f 100644 --- a/desktopApp/src/test/kotlin/org/meshtastic/desktop/ui/DesktopTopLevelDestinationParityTest.kt +++ b/desktopApp/src/test/kotlin/org/meshtastic/desktop/ui/DesktopTopLevelDestinationParityTest.kt @@ -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 { diff --git a/specs/20260520-153412-nav-tab-labels/checklists/requirements.md b/specs/20260520-153412-nav-tab-labels/checklists/requirements.md new file mode 100644 index 000000000..69664c280 --- /dev/null +++ b/specs/20260520-153412-nav-tab-labels/checklists/requirements.md @@ -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. diff --git a/specs/20260520-153412-nav-tab-labels/data-model.md b/specs/20260520-153412-nav-tab-labels/data-model.md new file mode 100644 index 000000000..efd2862e3 --- /dev/null +++ b/specs/20260520-153412-nav-tab-labels/data-model.md @@ -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) diff --git a/specs/20260520-153412-nav-tab-labels/plan.md b/specs/20260520-153412-nav-tab-labels/plan.md new file mode 100644 index 000000000..34d2a0339 --- /dev/null +++ b/specs/20260520-153412-nav-tab-labels/plan.md @@ -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 ` 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. diff --git a/specs/20260520-153412-nav-tab-labels/quickstart.md b/specs/20260520-153412-nav-tab-labels/quickstart.md new file mode 100644 index 000000000..d7a0e4480 --- /dev/null +++ b/specs/20260520-153412-nav-tab-labels/quickstart.md @@ -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 +Connect +Messages +``` + +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 | diff --git a/specs/20260520-153412-nav-tab-labels/research.md b/specs/20260520-153412-nav-tab-labels/research.md new file mode 100644 index 000000000..32f9dddc1 --- /dev/null +++ b/specs/20260520-153412-nav-tab-labels/research.md @@ -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. diff --git a/specs/20260520-153412-nav-tab-labels/spec.md b/specs/20260520-153412-nav-tab-labels/spec.md new file mode 100644 index 000000000..4ffeb41d2 --- /dev/null +++ b/specs/20260520-153412-nav-tab-labels/spec.md @@ -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 1–5) +- **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 1–5 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 diff --git a/specs/20260520-153412-nav-tab-labels/tasks.md b/specs/20260520-153412-nav-tab-labels/tasks.md new file mode 100644 index 000000000..73c7e0a5d --- /dev/null +++ b/specs/20260520-153412-nav-tab-labels/tasks.md @@ -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 T004–T009 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 (T001–T002) +2. Complete Phase 2: Rename enum entries (T003) +3. Complete Phase 3: Update all production references (T004–T009) — **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 (T011–T015) + +### 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