8000 feat(app): contextual messaging foundation (dismissible tips/info boxes) by shroominic · Pull Request #13 · shroominic/OpenGlucose · GitHub
[go: up one dir, main page]

Skip to content

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

Merged
shroominic merged 3 commits into
mainfrom
feat/contextual-messaging
Aug 12, 2026
Merged

shroominic merged 3 commits into
mainfrom
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

Model — lib/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.

Controller — lib/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).

Host — lib/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.

Wiring — lib/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.

shroominic and others added 3 commits June 23, 2026 19:24
…ator testing

Add a compile-time OG_DEMO build flag (--dart-define=OG_DEMO=true) so
native/simulator builds run the in-memory DemoCgmDriver instead of the
real BLE Aidex driver, and auto-scan + auto-connect on launch so the app
lands directly on the populated dashboard. The iOS simulator has no
Bluetooth, so this unblocks simulator-based feature verification.

When the flag is false (the default), behavior is unchanged: production
builds keep using the real AidexSensorDriver and the manual scan flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend the OG_DEMO demo driver into a scenario-driven mock so the whole
sensor lifecycle and alert surface can be exercised in the iOS simulator
without real BLE.

- Add MockScenarioCatalog (mock_scenarios.dart): pure, clock-injectable
  mapping from MockScenario -> CgmSessionSnapshot for warmup, activeNormal,
  activeHigh, activeLow, rapidRise, rapidFall, expiringSoon, expired,
  signalLoss, disconnected, multiSensorHistory, and error.
- Make DemoCgmDriver/DemoCgmSession scenario-driven with a live
  applyScenario() switch; default stays activeNormal so existing demo
  behavior is unchanged.
- Select the initial scenario via --dart-define=OG_SCENARIO=<name> and
  switch at runtime from a new "Mock scenario" picker in the Developer
  settings tab. Both paths gated behind OG_DEMO; production untouched.
- Tests: scenario->snapshot mapping, live scenario switching, and a widget
  test driving the Developer-tab picker end to end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
@shroominic
shroominic changed the base branch from feat/mock-sensor-scenarios to main August 12, 2026 17:22
@shroominic
shroominic merged commit 5195a19 into main Aug 12, 2026
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

0