Skip to content

feat: UI/UX polish — animations, haptics, Dynamic Type, a11y (PR 7/8)#17

Merged
billdmar merged 6 commits into
mainfrom
feat/pr7-ui-polish
Jun 29, 2026
Merged

feat: UI/UX polish — animations, haptics, Dynamic Type, a11y (PR 7/8)#17
billdmar merged 6 commits into
mainfrom
feat/pr7-ui-polish

Conversation

@billdmar

Copy link
Copy Markdown
Owner

What & why

Visual and accessibility polish on top of the feature set. Stacked on PR #16.

  • Typing indicator — an animated three-dot TypingIndicatorView replaces the plain spinner + label while the companion replies (Reduce-Motion-aware, announced as one VoiceOver status).
  • Message fade-in — new chat bubbles fade/slide in via a transition, gated on Reduce Motion.
  • Press feedback — a PressableButtonStyle (.pressable) gives quick-reply and send buttons a subtle scale/dim on press; skipped under Reduce Motion.
  • Centralized haptics — a Haptics enum (light/medium/escalation); the view model's private shims now delegate to it so feedback is consistent from one source.
  • Dynamic Type bubble fixChatBubble's cap is now screen-relative (~78%) instead of a fixed 300 pt, so long replies wrap and bubbles grow with Dynamic Type instead of clipping at large accessibility sizes — finally matching what the code comment always promised.
  • Accessibility identifiers on the key controls (settings, I'm safe, I need help, add-contact, contact fields, chat input/send/transcript) for the XCUITest flows in PR8.

Verification

  • xcodebuild test70/70 pass (polish is presentation-layer; no logic change).
  • swiftlint lint0 errors.
  • Animations/haptics/Dynamic Type are visual; verified on-device and exercised by the XCUITests added in PR8.

William Mar added 6 commits June 29, 2026 09:14
- Add .swiftlint.yml tuned for the codebase: high-value opt-in rules
  (force_unwrapping, empty_count, closure_spacing, explicit_init, …),
  SwiftUI-friendly disables (type_name for the Xcode-imposed @main name,
  multiple_closures_with_trailing_closure), and generous size thresholds
  for the legacy god view that the MVVM refactor is decomposing.
- Add a parallel SwiftLint CI job (non-strict: errors fail, warnings guide;
  tightens to --strict in a later PR once the god view is gone).
- Enable -enableCodeCoverage on the test run and publish an xccov summary to
  the GitHub Actions run summary (informational, non-gating).
- Add a SwiftLint badge to the README badge row.
- Drop one trailing comma in QuickReplies to clear a real lint error.

swiftlint lint: 0 errors. xcodebuild test: 46/46 pass.
Move all of the main screen's safety state and side effects out of the
view and into a new @mainactor SafetyWatcherViewModel: the check-in chat,
the live-location mirror, the three timers, contact persistence, and
escalation orchestration. SafetyWatcherView becomes presentation only,
reading the VM and forwarding intent.

Every dependency is injected so the controller is deterministically
testable without a device, network, UserDefaults, or wall-clock waits:
- LocationProviding   — LocationManager conforms; tests use a mock
- GeminiSending       — GeminiManager conforms; tests use a stub
- ContactStoring      — ContactStore wraps UserDefaults; tests use in-memory
- TimerScheduling     — RealTimerScheduler in the app; ManualTimerScheduler
                        fires ticks deterministically in tests
- now: () -> Date     — injectable clock for pure time-based assertions

Behavior is preserved (bug fixes come in a later PR). Adds
SafetyWatcherViewModelTests covering check-in ticks, inactivity escalation,
movement reset, mark-safe, walk start/arrive/overrun, contact add/remove/
load, and lifecycle. 46 -> 56 tests, all green; 0 SwiftLint errors.
Split each section of SafetyWatcherView into its own focused file under
Views/: StatusHeroView, MapCardView (now owns its own map-camera state),
WalkCardView, QuickActionsView, ChatCardView (transcript + quick replies +
input bar), ContactsCardView, AddContactSheet, and StartWalkSheet.

SafetyWatcherView becomes a ~60-line composition that wires the cards to
the shared view model — down from 1008 lines at the start of the series.

Now that the god view is gone, tighten the SwiftLint file_length /
type_body_length thresholds from the transitional values back toward
sensible limits (500/450 warn). No behavior change; 56/56 tests pass,
0 lint errors.
UI:
- Status hero pulse now uses symbolEffect isActive: — pulses only when
  attention is needed AND Reduce Motion is off (was running a pulse on
  every status change even under Reduce Motion).
- Map recenters only on the first fix and on an explicit new recenter
  button, instead of snapping back on every location update while the
  user is panning.
- Keyboard dismisses after sending a chat message (@focusstate).

Networking:
- GeminiError.invalidRequest distinguishes a request that couldn't be
  built (bad URL / encode failure) from a .decoding failure parsing the
  response (was misreported as .decoding).
- Conversation history is pruned to the system prompt + last 20 turns
  before each send via a pure GeminiManager.prune helper, bounding an
  ever-growing request on a long walk.

Escalation fail-safes (the documented High-severity gaps):
- Notification categories + delegate now register once at app launch
  (NotificationService.registerCategories in Party_WatcherApp.init),
  removing the race that could drop the action buttons on the first alert.
- Each escalation notification carries its contacts + coordinate as an
  immutable userInfo snapshot, read by the delegate at tap time — no more
  shared mutable singleton state that a rapid second escalation could clobber.
- UNUserNotificationCenter.add now has a completion handler; a delivery
  failure falls back to the in-app alert + a chat line pointing to the
  on-screen help button instead of failing silently.

Adds prune unit tests; updates SECURITY-REVIEW.md to mark the three High
fail-safe findings addressed. 56 -> 58 tests green, 0 lint errors.
…campus override

Adds the high-value feature set on top of the MVVM foundation:

- Settings screen (SettingsView + SafetySettings, persisted via SettingsStore):
  adjustable check-in cadence and inactivity threshold (fed straight into the
  already-parameterized SafetyEngine), background-location toggle, AI history
  depth, an optional campus emergency-number override (UTPD default preserved),
  and clear-chat. applySettings re-arms the timers atomically.
- Persistent chat history (ChatHistoryStore): the transcript + Gemini
  conversation survive relaunch, restored on init, bounded by pruning.
- Contextual Gemini errors: failed replies now show copy keyed off the
  GeminiError (no connection / busy / not configured / generic) instead of one
  opaque apology — always keeping the safety framing.
- Battery-aware warning (BatteryMonitoring): a banner when background tracking
  is on, the battery is <20%, and not charging. Guarded for the Simulator's
  unknown level.
- Minimal campus-number override: Escalation.callURL/displayNumber take an
  optional override and fall back to UTPD when blank/undialable; threaded
  through the notification + its userInfo. The pure builders + their tests
  stay intact.

Background-location toggle reaches LocationManager via a new
LocationProviding.setBackgroundUpdatesEnabled. README features + persistence
row updated.

Adds 12 unit tests (settings load/apply/toggle, chat restore/persist/clear,
contextual error copy, battery warning on/charging/off, plus Escalation
override + fallback). 58 -> 70 tests green, 0 lint errors.
- Animated three-dot TypingIndicatorView replaces the plain ProgressView
  + label while the companion is replying (Reduce-Motion-aware).
- Chat messages fade/slide in via a transition gated on Reduce Motion.
- PressableButtonStyle (.pressable) gives quick-reply, send, and quick-reply
  buttons a subtle scale/dim on press; skipped under Reduce Motion.
- Centralized Haptics helper (light/medium/escalation); the view model's
  private haptic shims now delegate to it for one consistent source.
- ChatBubble cap is now screen-relative (~78%) instead of a fixed 300pt, so
  long replies wrap and bubbles grow with Dynamic Type instead of clipping at
  large accessibility sizes — finally matching the comment's promise.
- accessibilityIdentifiers on the key controls (settings, I'm safe, I need
  help, add-contact, contact fields, chat input/send/transcript) for the
  XCUITest flows landing in PR8.

70/70 tests green, 0 lint errors.
@billdmar
billdmar changed the base branch from feat/pr6-features to main June 29, 2026 18:07
@billdmar
billdmar merged commit a79c193 into main Jun 29, 2026
@billdmar
billdmar deleted the feat/pr7-ui-polish branch June 29, 2026 18:21
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