feat(app): contextual messaging foundation (dismissible tips/info boxes) - #13
Open
shroominic wants to merge 1 commit into
Open
feat(app): contextual messaging foundation (dismissible tips/info boxes)#13shroominic wants to merge 1 commit into
shroominic wants to merge 1 commit into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.dartAppMessage:id,kind(tip|info|alert),title/body, optionaltriggerpredicate,dismissible,priority, andpersistence.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.dartChangeNotifier(matchesCgmAppControllerstyle).updateContext(MessageContext)recomputes the eligible set, honouring each message's trigger, dismissal state, and persistence; ordering ispriority→ kind (alert>info>tip) →id.topMessagesurfaces one message at a time to keep the dashboard calm.shared_preferences(aList<String>of dismissed ids); recurring dismissals are in-memory only.Host —
lib/src/messaging/message_host.dartMessageHostwidget: listens to the controller, renders the current top message as a clean, kind-styled, dismissible banner card with anAnimatedSwitcher(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)MessageControlleralongside the existingCgmAppController.MessageContext(built bymessage_context_builder.dartfrom session/warmup/readings) on each controller change, via a post-frame callback.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.dartinfo.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.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 atriggeroverMessageContextand the persistence they want. If they need a new state signal, they add a field toMessageContext+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 analyzeclean (one pre-existing info lint in an untouched file);flutter testgreen.Screenshots (iPhone 15 Pro, OG_DEMO)
OG_SCENARIO=warmup).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 pickerwidget test intest/widget_test.dart, which hangs underpumpAndSettledue 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.