Skip to content

feat(app): contextual messaging foundation (dismissible tips/info boxes) - #13

Open
shroominic wants to merge 1 commit into
feat/mock-sensor-scenariosfrom
feat/contextual-messaging
Open

feat(app): contextual messaging foundation (dismissible tips/info boxes)#13
shroominic wants to merge 1 commit into
feat/mock-sensor-scenariosfrom
feat/contextual-messaging

Conversation

@shroominic

Copy link
Copy Markdown
Owner

Contextual messaging foundation (TASK-004)

A dismissible, context/time-triggered in-app message subsystem. This is the engine + a demo that TIPS (TASK-005) and temporary INFO BOXES (TASK-006) both build on — they plug in as data (more AppMessages), with no controller or host changes.

Design

Modellib/src/messaging/app_message.dart

  • AppMessage: id, kind (tip | info | alert), title/body, optional trigger predicate, dismissible, priority, and persistence.
  • AppMessagePersistence: recurring (dismissal lasts the run), showOnce, showUntilDismissed (dismissal persisted forever).
  • MessageContext (message_context.dart): an immutable snapshot of app state (hasSession, isWarmingUp, hasReadings, now) that triggers read — so selection is a pure, testable function, never touching the live controller.

Controllerlib/src/messaging/message_controller.dart

  • A ChangeNotifier (matches CgmAppController style). updateContext(MessageContext) recomputes the eligible set, honouring each message's trigger, dismissal state, and persistence; ordering is priority → kind (alert > info > tip) → id.
  • topMessage surfaces one message at a time to keep the dashboard calm.
  • Dismissals persist via shared_preferences (a List<String> of dismissed ids); recurring dismissals are in-memory only.
  • Only notifies when the visible set actually changes (no spurious rebuilds).

Hostlib/src/messaging/message_host.dart

  • A self-contained MessageHost widget: listens to the controller, renders the current top message as a clean, kind-styled, dismissible banner card with an AnimatedSwitcher (size+fade) in/out. Renders zero-height when there's nothing to show, so it's safe to leave in the tree permanently.

Wiringlib/main.dart (minimal, separable)

  • Bootstrap builds a MessageController alongside the existing CgmAppController.
  • The dashboard pushes a fresh MessageContext (built by message_context_builder.dart from session/warmup/readings) on each controller change, via a post-frame callback.
  • One if (messageController != null) SliverToBoxAdapter(child: MessageHost(...)) inserted above the hero card. The param is optional, so other branches and existing tests are unaffected.

Sample messages (demo) — message_catalog.dart

  • Info box info.warmup — shown only during the warmup window: "Warming up — Your sensor is settling in. Readings begin after about an hour — no action needed." Auto-retires when warmup ends; dismissal persists.
  • Tip tip.tapReading — once live readings exist: "Tip — Tap a point on the chart to see the exact reading and time."

Content is honest / wellness-framed; no medical or treatment claims.

How tips / info-boxes plug in

TASK-005 and TASK-006 just append AppMessages to the catalog (tips = kind: tip, info boxes = kind: info), each with a trigger over MessageContext and the persistence they want. If they need a new state signal, they add a field to MessageContext + message_context_builder. The controller and host need no changes.

Tests

  • test/messaging/message_controller_test.dart — selection by trigger, null-trigger = always-on, priority/kind/id ordering, dismiss + isDismissed, non-dismissible no-op, persisted dismissal survives a fresh controller (next launch), recurring dismissal only hides for the current run, and notify-only-on-change.
  • test/messaging/message_host_test.dart — widget test: renders the top message, dismiss-on-tap hides it, and renders nothing when nothing is eligible.
  • flutter analyze clean (one pre-existing info lint in an untouched file); flutter test green.

Screenshots (iPhone 15 Pro, OG_DEMO)

  • Warmup info box (OG_SCENARIO=warmup).
  • After persisted dismissal + relaunch — info box gone, warmup card remains.
  • Tip in activeNormal.

Stacking

Stacked on #8 (mock-sensor harness). Rebase chain: #4#8 → this.

🤖 Generated with Claude Code

Test note

All suites pass (44 tests) except the pre-existing developer tab scenario picker widget test in test/widget_test.dart, which hangs under pumpAndSettle due to the dashboard hero card's 1s periodic ticker. Verified it hangs identically on the base branch (feat/mock-sensor-scenarios) with this PR's changes stashed out — it is pre-existing and unrelated to this change (this PR does not touch that test or the hero card). The other two widget tests and all unit tests pass.

Add a context/time-triggered, dismissible in-app message subsystem that
TIPS (TASK-005) and temporary INFO BOXES (TASK-006) both build on.

- Model (app_message.dart): AppMessage { id, kind (tip|info|alert),
  title/body, optional trigger predicate over MessageContext, dismissible,
  priority, persistence (recurring | showOnce | showUntilDismissed) }.
  MessageContext is an immutable app-state snapshot so selection is pure.
- Controller (message_controller.dart): a ChangeNotifier that selects the
  eligible messages for the current context (trigger + dismissal +
  priority/kind ordering), surfaces one at a time, and persists dismissals
  via shared_preferences. Notifies only when the visible set changes.
- Host (message_host.dart): self-contained dismissible banner card with a
  size+fade AnimatedSwitcher; renders zero-height when nothing is eligible.
- Wiring (main.dart): build a MessageController in bootstrap, push a fresh
  MessageContext on each controller change, and insert one MessageHost
  sliver above the hero card. The param is optional so other branches and
  existing tests are unaffected.
- Demo content (message_catalog.dart): a warmup info box and a chart tip,
  honest/wellness-framed. TASK-005/006 plug in as more AppMessages.

Tests: controller selection/ordering/dismiss/persistence + a host widget
test. flutter analyze clean.

Refs: TASK-004

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant