refactor(folders): consolidate, externalize, and memoize folder-label resolution #120

Merged
JMR-dev merged 2 commits from refactor-folder-label-resolver into main 2026-07-02 09:14:32 +00:00
JMR-dev commented 2026-07-02 09:02:02 +00:00 (Migrated from github.com)

Three code-quality cleanups from the PR #54 review, all in the folder-label resolution / provider-label code, so they ship as one change (bundling avoids the collisions three separate PRs would cause). Each was adapted to the current code, which changed after #108 (shared resolvedFolderLabels + providerLabelFor) and #117 (unified attribute→role table) merged.

Closes #67, #68, #69

#69 — providerLabel: consolidate provider-brand host matching

Host→brand knowledge now lives in one place, MailProvider:

  • forImapHost matches an entry's imapHost plus new hostAliases — Gmail gains its legacy imap.googlemail.com host (fixes the under-match that rendered "Drafts - gmail.com").
  • A new companion brandFor(account) is the single host→brand seam. It recognizes Outlook by OAuth auth type or a precise Microsoft host (*.office365.com / outlook.office.com) — no longer any host merely containing "outlook"/"office365" (fixes the over-match).
  • MailProvider stays the app-password preset registry; Outlook is deliberately not an entry (it uses interactive OAuth, and entries drives the app-password picker), so brand recognition is added in the companion rather than as a preset.
  • providerLabel() drops its ad-hoc host substrings and just calls MailProvider.brandFor(account), falling back to the email domain.

#68 — i18n: move folder-label disambiguation patterns into strings.xml

The hardcoded English grammars ("base - provider", "base (parent)", "base [path]") become folder_label_with_provider / _with_parent / _with_path string resources. They are threaded into the pure resolver as a LabelPatterns bundle whose defaults match the old literals, so resolveDrawerLabels stays pure and JVM-testable; the @Composable call site resolves the localized strings and passes them down. Translators can now see and reorder/re-punctuate these.

#67 — FolderDrawer: memoize resolution, resolver early-return, fail-fast lookups

  • Memoize: resolvedFolderLabels hoists the role→string and pattern stringResource lookups out of a remember(folders, accounts, roleLabels, patterns) calculation, so the resolved map is rebuilt only when those inputs change — not on every recomposition of a caller that composes while idle (the closed navigation drawer). The stable map instance also lets strong-skipping reuse unchanged row label lambdas.
  • Early-return: resolveDrawerLabels returns baseLabels unchanged in the dominant no-collision case instead of rebuilding a value-identical map.
  • Fail-fast: both former ?: fallbacks (in the resolver and the drawer row) now use getValue, so a key miss fails loudly instead of silently reverting to un-deduplicated labels.

Behavior unchanged

This is pure cleanup — same outputs. The existing assertions guard it:

  • FolderLabelsTest (JVM) — the provider-suffix, nested-parent, top-level tie-break, and safety-net cases from #60/#61/#64/#108 all stay green (they call the resolver with default patterns).
  • FolderDrawerTest (androidTest) — "Drafts - Gmail" / "Drafts - Outlook" rendering and the account-switch-gap suffix still pass (compiled here; runs in CI E2E).

Tests added

  • MailProviderTest: forImapHost matches primary hosts + the googlemail alias (case-insensitive); matchesHost accepts aliases but not unrelated hosts; brandFor centralizes host→brand incl. Outlook, with an over-match guard (outlook.example.com → no brand).
  • FolderLabelsTest: providerLabel via the googlemail alias, a manually-configured Office 365 host without OAuth, and the over-match guard (#69); resolver formats via supplied provider/parent/path patterns (#68); early-return returns the same baseLabels instance, fails fast on a missing base label, and is deterministic for equal inputs (#67).

For the maintainer

MailProvider gained a host→brand concern (brandFor) alongside the app-password presets — kept there (vs. a new brand enum) to avoid duplicating the preset hosts. Outlook host matching is intentionally narrow (*.office365.com, outlook.office.com); personal outlook.com/hotmail.com accounts are OAuth and matched by auth type. Scope was held to the resolver / providerLabel / FolderDrawer / strings.xml (+ the MailProvider registry #69 targets); no DB/entity/schema changes, and #66 (persist IMAP delimiter) was left out as requested.

🤖 Generated with Claude Code

Three code-quality cleanups from the PR #54 review, all in the folder-label resolution / provider-label code, so they ship as one change (bundling avoids the collisions three separate PRs would cause). Each was adapted to the current code, which changed after #108 (shared `resolvedFolderLabels` + `providerLabelFor`) and #117 (unified attribute→role table) merged. ## Closes #67, #68, #69 ### #69 — providerLabel: consolidate provider-brand host matching Host→brand knowledge now lives in one place, `MailProvider`: - `forImapHost` matches an entry's `imapHost` **plus new `hostAliases`** — Gmail gains its legacy `imap.googlemail.com` host (fixes the under-match that rendered "Drafts - gmail.com"). - A new companion `brandFor(account)` is the single host→brand seam. It recognizes Outlook by OAuth auth type **or a precise Microsoft host** (`*.office365.com` / `outlook.office.com`) — no longer any host merely *containing* "outlook"/"office365" (fixes the over-match). - `MailProvider` stays the app-password preset registry; Outlook is deliberately **not** an entry (it uses interactive OAuth, and `entries` drives the app-password picker), so brand recognition is added in the companion rather than as a preset. - `providerLabel()` drops its ad-hoc host substrings and just calls `MailProvider.brandFor(account)`, falling back to the email domain. ### #68 — i18n: move folder-label disambiguation patterns into strings.xml The hardcoded English grammars (`"base - provider"`, `"base (parent)"`, `"base [path]"`) become `folder_label_with_provider` / `_with_parent` / `_with_path` string resources. They are threaded into the pure resolver as a `LabelPatterns` bundle whose defaults match the old literals, so `resolveDrawerLabels` stays pure and JVM-testable; the `@Composable` call site resolves the localized strings and passes them down. Translators can now see and reorder/re-punctuate these. ### #67 — FolderDrawer: memoize resolution, resolver early-return, fail-fast lookups - **Memoize:** `resolvedFolderLabels` hoists the role→string and pattern `stringResource` lookups out of a `remember(folders, accounts, roleLabels, patterns)` calculation, so the resolved map is rebuilt only when those inputs change — not on every recomposition of a caller that composes while idle (the closed navigation drawer). The stable map instance also lets strong-skipping reuse unchanged row label lambdas. - **Early-return:** `resolveDrawerLabels` returns `baseLabels` unchanged in the dominant no-collision case instead of rebuilding a value-identical map. - **Fail-fast:** both former `?:` fallbacks (in the resolver and the drawer row) now use `getValue`, so a key miss fails loudly instead of silently reverting to un-deduplicated labels. ## Behavior unchanged This is pure cleanup — same outputs. The existing assertions guard it: - `FolderLabelsTest` (JVM) — the provider-suffix, nested-parent, top-level tie-break, and safety-net cases from #60/#61/#64/#108 all stay green (they call the resolver with default patterns). - `FolderDrawerTest` (androidTest) — "Drafts - Gmail" / "Drafts - Outlook" rendering and the account-switch-gap suffix still pass (compiled here; runs in CI E2E). ## Tests added - `MailProviderTest`: `forImapHost` matches primary hosts + the googlemail alias (case-insensitive); `matchesHost` accepts aliases but not unrelated hosts; `brandFor` centralizes host→brand incl. Outlook, with an over-match guard (`outlook.example.com` → no brand). - `FolderLabelsTest`: `providerLabel` via the googlemail alias, a manually-configured Office 365 host without OAuth, and the over-match guard (#69); resolver formats via supplied provider/parent/path patterns (#68); early-return returns the same `baseLabels` instance, fails fast on a missing base label, and is deterministic for equal inputs (#67). ## For the maintainer `MailProvider` gained a host→brand concern (`brandFor`) alongside the app-password presets — kept there (vs. a new brand enum) to avoid duplicating the preset hosts. Outlook host matching is intentionally narrow (`*.office365.com`, `outlook.office.com`); personal outlook.com/hotmail.com accounts are OAuth and matched by auth type. Scope was held to the resolver / providerLabel / FolderDrawer / strings.xml (+ the `MailProvider` registry #69 targets); no DB/entity/schema changes, and #66 (persist IMAP delimiter) was left out as requested. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.