From ebde12fb53ba25ace7dbd51f134a3c5b7b45f390 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sun, 9 Aug 2026 19:10:51 -0700 Subject: [PATCH 1/3] refactor(Where): retire AI recent activity summaries --- .github/snapshot-shards.json | 1 - Where/AGENTS.md | 5 - Where/TODOs.md | 6 +- Where/Where/Sources/WhereShortcuts.swift | 9 - Where/WhereCore/README.md | 10 +- .../Logging/RecentActivitySummarizerLog.swift | 32 -- .../WhereCore/Sources/Logging/WhereLog.swift | 5 +- .../FoundationModelSummaryGenerator.swift | 87 ---- .../RecentActivitySummarizer.swift | 195 --------- .../RecentActivity/RecentActivityWindow.swift | 46 -- Where/WhereCore/Sources/WhereServices.swift | 18 - .../Tests/Logging/WhereLogTests.swift | 1 - .../Tests/RecentActivitySummarizerTests.swift | 199 --------- .../Tests/RecentActivityWindowTests.swift | 43 -- Where/WhereIntents/README.md | 8 +- .../Sources/ActivityWindowAppEnum.swift | 42 -- .../WhereIntents/Sources/IntentStrings.swift | 38 -- .../Sources/Logging/WhereIntentsLog.swift | 19 +- .../Sources/RecentActivitySummaryIntent.swift | 53 --- .../Sources/Resources/Localizable.xcstrings | 127 ------ .../Sources/WhereIntentReader.swift | 12 +- .../Tests/ActivityWindowAppEnumTests.swift | 17 - .../Tests/IntentStringsTests.swift | 29 +- .../Tests/WhereIntentsLogTests.swift | 10 +- ...centActivitySummaryViewSnapshotTests.swift | 10 - .../insightsAccuracyFeatures.Default_iPad.png | 4 +- ...acyFeatures.Default_iPad_accessibility.png | 4 +- ...ightsAccuracyFeatures.Default_iPad_ax5.png | 4 +- ...AccuracyFeatures.Default_iPad_contrast.png | 4 +- ...ghtsAccuracyFeatures.Default_iPad_dark.png | 4 +- ...nsightsAccuracyFeatures.Default_iPhone.png | 4 +- ...yFeatures.Default_iPhone_accessibility.png | 4 +- ...htsAccuracyFeatures.Default_iPhone_ax5.png | 4 +- ...curacyFeatures.Default_iPhone_contrast.png | 4 +- ...tsAccuracyFeatures.Default_iPhone_dark.png | 4 +- .../recentActivity.Empty_iPhone.png | 3 - .../recentActivity.Empty_iPhone_dark.png | 3 - .../recentActivity.Failed_iPhone.png | 3 - .../recentActivity.Failed_iPhone_dark.png | 3 - .../recentActivity.Loaded_iPad.png | 3 - ...centActivity.Loaded_iPad_accessibility.png | 3 - .../recentActivity.Loaded_iPad_ax5.png | 3 - .../recentActivity.Loaded_iPad_contrast.png | 3 - .../recentActivity.Loaded_iPad_dark.png | 3 - .../recentActivity.Loaded_iPhone.png | 3 - ...ntActivity.Loaded_iPhone_accessibility.png | 3 - .../recentActivity.Loaded_iPhone_ax5.png | 3 - .../recentActivity.Loaded_iPhone_contrast.png | 3 - .../recentActivity.Loaded_iPhone_dark.png | 3 - .../recentActivity.Unavailable_iPhone.png | 3 - ...recentActivity.Unavailable_iPhone_dark.png | 3 - .../siriFeatures.Default_iPad.png | 4 +- ...iriFeatures.Default_iPad_accessibility.png | 4 +- .../siriFeatures.Default_iPad_ax5.png | 4 +- .../siriFeatures.Default_iPad_contrast.png | 4 +- .../siriFeatures.Default_iPad_dark.png | 4 +- .../siriFeatures.Default_iPhone.png | 4 +- ...iFeatures.Default_iPhone_accessibility.png | 4 +- .../siriFeatures.Default_iPhone_ax5.png | 4 +- .../siriFeatures.Default_iPhone_contrast.png | 4 +- .../siriFeatures.Default_iPhone_dark.png | 4 +- .../year.Empty_iPhone.png | 4 +- .../year.Empty_iPhone_dark.png | 4 +- .../year.Loaded_iPad.png | 4 +- .../year.Loaded_iPad_accessibility.png | 4 +- .../year.Loaded_iPad_ax5.png | 4 +- .../year.Loaded_iPad_contrast.png | 4 +- .../year.Loaded_iPad_dark.png | 4 +- .../year.Loaded_iPhone.png | 4 +- .../year.Loaded_iPhone_accessibility.png | 4 +- .../year.Loaded_iPhone_ax5.png | 4 +- .../year.Loaded_iPhone_contrast.png | 4 +- .../year.Loaded_iPhone_dark.png | 4 +- .../Flyover/WhereFlyoverCatalog.swift | 1 - .../Logging/RecentActivityModelLog.swift | 24 -- .../Sources/Model/RecentActivityModel.swift | 67 --- .../Sources/Preview/PreviewSupport.swift | 13 - .../Primary/RecentActivitySummaryView.swift | 187 -------- .../Sources/Resources/Localizable.xcstrings | 403 +----------------- .../Sources/Settings/AppIconOption.swift | 4 +- .../FeatureRecentActivityPreview.swift | 44 -- .../InsightsAccuracyFeaturesView.swift | 64 +-- .../Shared/FeatureDiscoveryPresentation.swift | 49 +-- .../Siri/SiriFeaturesView.swift | 2 - .../Siri/SiriIntentFeature.swift | 5 - .../Sources/Settings/SettingsView.swift | 1 - .../Shared/AppIconActivityIndicator.swift | 3 +- .../Sources/Shared/TypewriterSchedule.swift | 39 -- .../Sources/Shared/TypewriterText.swift | 68 --- .../WhereUI/Sources/Shared/WhereFormat.swift | 53 --- Where/WhereUI/Sources/Year/YearView.swift | 20 +- .../FeatureDiscoveryPresentationTests.swift | 35 -- .../Tests/RecentActivityModelTests.swift | 136 ------ .../Tests/TypewriterScheduleTests.swift | 58 --- 94 files changed, 101 insertions(+), 2373 deletions(-) delete mode 100644 Where/WhereCore/Sources/Logging/RecentActivitySummarizerLog.swift delete mode 100644 Where/WhereCore/Sources/RecentActivity/FoundationModelSummaryGenerator.swift delete mode 100644 Where/WhereCore/Sources/RecentActivity/RecentActivitySummarizer.swift delete mode 100644 Where/WhereCore/Sources/RecentActivity/RecentActivityWindow.swift delete mode 100644 Where/WhereCore/Tests/RecentActivitySummarizerTests.swift delete mode 100644 Where/WhereCore/Tests/RecentActivityWindowTests.swift delete mode 100644 Where/WhereIntents/Sources/ActivityWindowAppEnum.swift delete mode 100644 Where/WhereIntents/Sources/RecentActivitySummaryIntent.swift delete mode 100644 Where/WhereIntents/Tests/ActivityWindowAppEnumTests.swift delete mode 100644 Where/WhereUI/SnapshotTests/RecentActivitySummaryViewSnapshotTests.swift delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Empty_iPhone.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Empty_iPhone_dark.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Failed_iPhone.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Failed_iPhone_dark.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPad.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPad_accessibility.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPad_ax5.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPad_contrast.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPad_dark.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPhone.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPhone_accessibility.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPhone_ax5.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPhone_contrast.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPhone_dark.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Unavailable_iPhone.png delete mode 100644 Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Unavailable_iPhone_dark.png delete mode 100644 Where/WhereUI/Sources/Logging/RecentActivityModelLog.swift delete mode 100644 Where/WhereUI/Sources/Model/RecentActivityModel.swift delete mode 100644 Where/WhereUI/Sources/Primary/RecentActivitySummaryView.swift delete mode 100644 Where/WhereUI/Sources/Settings/FeaturePreviews/InsightsAccuracy/FeatureRecentActivityPreview.swift delete mode 100644 Where/WhereUI/Sources/Shared/TypewriterSchedule.swift delete mode 100644 Where/WhereUI/Sources/Shared/TypewriterText.swift delete mode 100644 Where/WhereUI/Tests/RecentActivityModelTests.swift delete mode 100644 Where/WhereUI/Tests/TypewriterScheduleTests.swift diff --git a/.github/snapshot-shards.json b/.github/snapshot-shards.json index e790a0ee2..75f13039e 100644 --- a/.github/snapshot-shards.json +++ b/.github/snapshot-shards.json @@ -34,7 +34,6 @@ "WhereUISnapshotTests/DayRelabelViewSnapshotTests", "WhereUISnapshotTests/OnboardingViewSnapshotTests", "WhereUISnapshotTests/PersonalizationFeaturesViewSnapshotTests", - "WhereUISnapshotTests/RecentActivitySummaryViewSnapshotTests", "WhereUISnapshotTests/RegionCustomizeViewSnapshotTests", "WhereUISnapshotTests/RegionDaysViewSnapshotTests", "WhereUISnapshotTests/ResolutionViewSnapshotTests", diff --git a/Where/AGENTS.md b/Where/AGENTS.md index 65c8abc43..9b63af889 100644 --- a/Where/AGENTS.md +++ b/Where/AGENTS.md @@ -76,11 +76,6 @@ Rules the code enforces and agents must preserve: - **Manual entries carry a `ManualEntryAudit`**; `DayJournal`'s write methods take an explicit `audit:` (no default). An additive backfill can't downgrade an authoritative row's regions, but the newer audit always wins. -- **`WhereServices.recentActivity`** (the on-demand Foundation Models - summarizer, behind `ActivitySummaryGenerating`) is distinct from - `WhereServices.summary` (the daily notification recap); model unavailability - surfaces as a typed reason, never a silent empty summary. - ## Spans Anything plausibly expensive is measured — `logger.measure(.name, budget:)` on diff --git a/Where/TODOs.md b/Where/TODOs.md index cc7265dc5..239d0ab71 100644 --- a/Where/TODOs.md +++ b/Where/TODOs.md @@ -73,8 +73,8 @@ The item format and the placement rule live in the root - test(WhereShareExtension, WhereWidgets) [quick-win]: Close the two extension-shaped test gaps that don't need a new bundle pattern decision — `ShareEvidenceModel.buildPendingEvidence()` (`ShareEvidenceModel.swift:126-132`, documented as exposed for testing at `:124-125`, nothing tests it) and `WhereWidgetProvider`'s midnight reload policy (`WhereWidgetProvider.swift:34-41`, `.after(nextMidnight)` — the extension's core scheduling logic, untested in any target). (audit 2026-07-26) - test(RegionKit) [quick-win]: Add `GeoJSONTests.swift`. The unsupported-geometry throw (`GeoJSON.swift:62-67`) and malformed-coordinate drop (`:124-128`) are untested, and `RegionCatalog.loadFromBundle()`'s degrade-to-empty-catalog behavior is asserted only at the log-event level (`RegionLogTests.swift:27-30`) rather than at runtime. `RegionKit/README.md:161-162` meanwhile claims GeoJSON decoding *is* covered — filed in the docs item below, and closing this item is what would make the README true. (audit 2026-07-26; re-verified 2026-08-09) - test(WhereUI) [quick-win]: Add the missing namesake tests for `LocationNamer` (cache / coalescing) and `CalendarContentView`'s `scrolledForYear` scroll-reveal gate, which has hosting smoke only. (audit 2026-07-26) -- test(WhereIntents) [quick-win]: Test `RegionSpotlightIndexer` and `WhereIntentReader.recentActivity`. The indexer **shipped in PR #210** (`RegionEntity+Spotlight.swift:16-33`, wired in `Where/Where/Sources/RegularApplicationRuntime.swift` with a demo-mode skip) and arrived with no test of its own: nothing references `RegionSpotlightIndexer` or `indexRegions`, so neither the success log, the degraded failure log, nor the demo skip is verified. `RegionEntityTests.swift:27-45` covers `RegionEntity.tracked(from:)` — the indexer's *input* — which is easy to mistake for coverage of the indexer. `WhereIntentReaderTests` still has no `recentActivity` case. (audit 2026-07-26; re-verified 2026-08-09) -- test(WhereCore) [needs-design]: Close the namesake-test debt — **59** of the 118 implementation files have no same-named `*Tests.swift` (re-derived by basename 2026-08-09; was 28 of 87 at the July 26 audit, so the debt grew with the module rather than being worked down). Still uncovered among the originally named files: `FoundationModelSummaryGenerator`, `WidgetTimelineRefresher`, `BackupArchive` (`WherePreferences` closed 2026-08-05). `WhereCoreTests.swift` is an omnibus holding five suites, not just `YearReportTests` — also `SwiftDataStoreFactoryTests`, `SDLocationSampleTests`, `EvidenceKindTests`, `SampleSourceTests`. The basename count is a proxy: `Sources/Logging/*` event types and `SD*` record shells reasonably have no namesake file, so treat it as a trend line, not a work list. Split by concern as those files change rather than in one pass. (audit 2026-07-26) +- test(WhereIntents) [quick-win]: Test `RegionSpotlightIndexer`. The indexer **shipped in PR #210** (`RegionEntity+Spotlight.swift:16-33`, wired in `Where/Where/Sources/RegularApplicationRuntime.swift` with a demo-mode skip) and arrived with no test of its own: nothing references `RegionSpotlightIndexer` or `indexRegions`, so neither the success log, the degraded failure log, nor the demo skip is verified. `RegionEntityTests.swift:27-45` covers `RegionEntity.tracked(from:)` — the indexer's *input* — which is easy to mistake for coverage of the indexer. (audit 2026-07-26; re-verified 2026-08-09) +- test(WhereCore) [needs-design]: Close the namesake-test debt — **57** of the 114 implementation files have no same-named `*Tests.swift` (re-derived by basename 2026-08-09; was 28 of 87 at the July 26 audit, so the debt grew with the module rather than being worked down). Still uncovered among the originally named files: `WidgetTimelineRefresher`, `BackupArchive` (`WherePreferences` closed 2026-08-05). `WhereCoreTests.swift` is an omnibus holding five suites, not just `YearReportTests` — also `SwiftDataStoreFactoryTests`, `SDLocationSampleTests`, `EvidenceKindTests`, `SampleSourceTests`. The basename count is a proxy: `Sources/Logging/*` event types and `SD*` record shells reasonably have no namesake file, so treat it as a trend line, not a work list. Split by concern as those files change rather than in one pass. (audit 2026-07-26) - docs(WhereCore) [quick-win]: Refresh stale doc claims — `WhereCore/README.md:66-67` claims every write reconciles (it omits summary and `setPrimaryRegions`, both filed above) and `:271-272` claims errors are never swallowed into an empty default (the badge path above contradicts it); `LocationIngestor.swift:473-474` says it logs through `os.Logger` when the code immediately below emits typed `WhereLog` events (`:477-481`); `RegionViewer/README.md:17-18` describes its **Source** mode as decoding "all US-state features in `us-states.geojson`", but `RegionGeometryCatalog.buildSourceOutlines()` (`:146`) walks `RegionCatalog.shared.entries` and decodes each region's **bundled per-region** GeoJSON — 54 files under `RegionKit/Sources/Resources/regions/`. The monolith still exists at `RegionKit/Tools/source/us-states.geojson`, but it is a build-time input to the extraction tooling, never bundled and never read at runtime, so the README describes the pipeline's input as if it were the app's data. The same file's hand-listed region set is stale for the same reason. `RegionKit/README.md:161-162` also claims GeoJSON decoding is covered, which the test item above would make true instead. (Two halves already closed: the `RootView.swift` "four screens" doc now reads three tabs, and the `WhereShareExtension/AGENTS.md:21` compose-model credit was fixed 2026-07-27.) (audit 2026-07-26; citations refreshed 2026-08-09) - refactor(WhereCore) [quick-win]: Drop the remaining Core-API parameter defaults — `DayJournal.addEvidence(_:blob:)` (`DayJournal.swift:271`) and `WidgetDataReader`'s aggregator/attributor (`WidgetDataReader.swift:77-78`). The composition root already knows each value. (Two of the four filed are now done: `WherePreferences.init(store:)` and `SwiftDataStore.make(storage:)` both require the argument.) (audit 2026-07-26; re-verified 2026-08-09) - convention(WhereIntents) [quick-win]: Small polish, all four parts still open — register `LogTripIntent` in `WhereShortcuts` (`Where/Where/Sources/WhereShortcuts.swift:11-56` registers five, no trip backfill) or document Shortcuts-only discovery; use `Calendar.whereIntents` for `LogDayIntent`'s default day instead of `date ?? Date()` (`LogDayIntent.swift:39`, no data impact today since `DayJournal` buckets Gregorian); log the App Group open failure behind `WhereIntentReader.todaySnapshot`'s `try?` (`WhereIntentReader.swift:17-18`); and wrap `RegionViewer`'s `RegionMapView` in `.whereBroadwayRoot()` (`RegionViewerApp.swift:15-18`) so the dev tool renders with app styling. (audit 2026-07-26; re-verified 2026-08-09) @@ -84,7 +84,7 @@ The item format and the placement rule live in the root - feat(WhereUI): Raw data browser (similar to the SwiftData browser). (human) - docs(WhereUI): Add comments to strings in the xcstrings files. (human) - refactor(WhereUI) [quick-win]: The widget fixtures' pinned instant is still `1_770_000_000` (02:40 UTC / Feb 1 evening Pacific, near a day boundary) at `PreviewSupport.swift:611` and `:652`. Moving it safely off midnight was skipped to avoid re-recording the widget references; PR #196's re-record makes that cheap now. (From the July 2026 snapshot-testing PR review; re-verified 2026-08-09) -- test(WhereUI) [needs-design]: Snapshot matrix gaps — `RecentActivitySummaryView.loading` (the sole user of `AppIconActivityIndicator`, so its `@MotionIsStatic` pinning is the one motion adoption without direct capture coverage; the suite covers only Loaded/Empty/Unavailable/Failed at `:148-171`), an explicit `LocationsView` empty state, and `ManualDayView`'s range-mode add have no snapshot case. (`YearView` gained its `Empty` case at `YearView.swift:149-151`.) (From the July 2026 snapshot-testing PR review; re-verified 2026-08-09) +- test(WhereUI) [needs-design]: Snapshot matrix gaps — an explicit `LocationsView` empty state and `ManualDayView`'s range-mode add have no snapshot case. (`YearView` gained its `Empty` case at `YearView.swift:149-151`.) (From the July 2026 snapshot-testing PR review; re-verified 2026-08-09) - test(WhereUI) [quick-win]: Three screens have a `#Preview` but no `SnapshotProviding` conformance, so no image pins them — against the module convention that an image bundle, not a hosting smoke test, owns "does this screen render". They are the only Settings drill-ins without coverage: `AlertsSettingsView` (reminders, daily summary, issue alerts, the drift threshold, and a manual "find issues now"), `VisibleYearSettingsView`, and `RemovedDeviceView` (`Devices/RemovedDeviceView.swift`), the blocking CloudKit-removal recovery gate with the rejoin call to action — a screen a user only reaches when something has already gone wrong, which is the worst place for an unpinned regression. **Two of the three are not new debt:** Alerts and VisibleYear have been uncovered since PR #111 landed the drill-in restyle, so three prior audits missed them; only `RemovedDeviceView` arrived with PR #160. Every other screen added in this window — About, Devices, License, LifecycleFailure, ShareEvidence, Siri, Widget, CardDesignerStudio, Flyover root — did get coverage, so the convention holds and these are the exceptions. Add cases following `Settings/DevicesSettingsView.swift` (for Alerts, an authorized and a denied-notifications variant, toggles on and off). (audit 2026-08-09) ## Deferred snapshot-test flakiness diff --git a/Where/Where/Sources/WhereShortcuts.swift b/Where/Where/Sources/WhereShortcuts.swift index 61df6d8da..91c39104e 100644 --- a/Where/Where/Sources/WhereShortcuts.swift +++ b/Where/Where/Sources/WhereShortcuts.swift @@ -36,15 +36,6 @@ struct WhereShortcuts: AppShortcutsProvider { shortTitle: "Region on a Date", systemImageName: "calendar.badge.clock", ) - AppShortcut( - intent: RecentActivitySummaryIntent(), - phrases: [ - "Summarize my recent activity in \(.applicationName)", - "Where have I been in \(.applicationName)", - ], - shortTitle: "Recent Activity", - systemImageName: "sparkles", - ) AppShortcut( intent: LogDayIntent(), phrases: [ diff --git a/Where/WhereCore/README.md b/Where/WhereCore/README.md index 6f63d8c4f..e713e96be 100644 --- a/Where/WhereCore/README.md +++ b/Where/WhereCore/README.md @@ -3,9 +3,8 @@ The domain layer of the **Where** app: it ingests location, persists it, rolls it up into per-day and per-year region presence, finds the data-quality problems worth resolving, and drives the side effects that follow a change (reminders, -widget snapshots, backups, on-device activity summaries). It is pure Swift + -Foundation + SwiftData + CoreLocation + FoundationModels — **no SwiftUI or -UIKit** — so all of it is unit-testable off-screen. It builds on +widget snapshots, backups). It is pure Swift + Foundation + SwiftData + +CoreLocation — **no SwiftUI or UIKit** — so all of it is unit-testable off-screen. It builds on [`RegionKit`](../RegionKit) for coordinate→region lookup and logs through [`Periscope`](../../Shared/Periscope) via the `WhereLog` facade. @@ -154,8 +153,6 @@ one it belongs to rather than to a god-object: sidecar tombstone before clearing recovery, so a cold launch can repair a preference write that did not reach disk without offering the same archive again. Check-ins are deliberately neither exported nor restored because they are live advisory status. -- **`RecentActivitySummarizer`** — an on-device Foundation Models narrative over - a selectable look-back `RecentActivityWindow`. - **`InstallationRecordingContext`** — the device-local installation identity, explicitly confirmed local recording choice, and stable timestamp for recreating its immutable device profile idempotently. @@ -270,9 +267,6 @@ rotates to a Reset child generation, and discards the retry queue only after com incomplete causal generation DAG fails closed instead of mixing old and new state. - **Failures surface.** Store methods are `async throws`; errors are logged via `WhereLog` and left observable — never swallowed into an empty default. -- **Foundation Models may be unavailable.** `RecentActivitySummarizer` reports a - typed reason rather than a silently empty summary. - ## Testing Swift Testing in [`Tests/`](Tests) (`WhereCoreTests`), hosted in `StuffTestHost`. diff --git a/Where/WhereCore/Sources/Logging/RecentActivitySummarizerLog.swift b/Where/WhereCore/Sources/Logging/RecentActivitySummarizerLog.swift deleted file mode 100644 index ba724af7a..000000000 --- a/Where/WhereCore/Sources/Logging/RecentActivitySummarizerLog.swift +++ /dev/null @@ -1,32 +0,0 @@ -import PeriscopeCore - -/// Structured events for `RecentActivitySummarizer`, the on-device look-back -/// summary. Both outcomes are `.info` — a skip (no samples) and a successful -/// generation. -enum RecentActivitySummarizerLog: LogEvent { - /// Names the summarizer's timed spans. - enum SpanName: Hashable { - /// Attributing every reading in the window to a region and collapsing the - /// runs into dwell segments. A point-in-polygon test per reading, and a - /// long window holds thousands, so it's worth separating from the model - /// call it feeds — a slow summary is otherwise assumed to be the model's - /// fault. - case attribute - /// The on-device model call. - case generate - } - - case skippedNoSamples - case generated(segmentCount: Int) - - static let eventName = "RecentActivitySummarizer" - - var message: String { - switch self { - case .skippedNoSamples: - "Recent-activity summary skipped: no samples in window" - case let .generated(segmentCount): - "Recent-activity summary generated from \(segmentCount) segment(s)" - } - } -} diff --git a/Where/WhereCore/Sources/Logging/WhereLog.swift b/Where/WhereCore/Sources/Logging/WhereLog.swift index 9454be72e..dc9dddb02 100644 --- a/Where/WhereCore/Sources/Logging/WhereLog.swift +++ b/Where/WhereCore/Sources/Logging/WhereLog.swift @@ -16,7 +16,7 @@ public struct WhereRoot: LogEvent { /// Every logger derives from one `"Where"` root `Log` and emits into the /// process-wide Periscope system (``Periscope/shared``). Collaborators that /// belong together sit under a shared group scope (``location``, ``reminders``, -/// ``backup``, ``widgets``, ``session``, ``evidence``, ``recentActivity``); +/// ``backup``, ``widgets``, ``session``, ``evidence``); /// everything else hangs directly off ``root``. A collaborator derives its own /// typed leaf — `WhereLog.location(LocationIngestorLog.self)` — so its events /// carry a structured payload the log viewer can decode, and the loggers form a @@ -45,8 +45,6 @@ public enum WhereLog { public static let session = group(.session) /// Evidence capture/list/detail view models. public static let evidence = group(.evidence) - /// On-device recent-activity summarization. - public static let recentActivity = group(.recentActivity) /// The read/derive path everything else consumes: year reports, calendar /// layout, and the data-issue scan. Mostly a span subtree — these /// collaborators throw their failures rather than logging them, so what's @@ -66,7 +64,6 @@ public enum WhereLog { case widgets case session case evidence - case recentActivity case reporting } } diff --git a/Where/WhereCore/Sources/RecentActivity/FoundationModelSummaryGenerator.swift b/Where/WhereCore/Sources/RecentActivity/FoundationModelSummaryGenerator.swift deleted file mode 100644 index cadc70096..000000000 --- a/Where/WhereCore/Sources/RecentActivity/FoundationModelSummaryGenerator.swift +++ /dev/null @@ -1,87 +0,0 @@ -import Foundation -import FoundationModels - -/// On-device `ActivitySummaryGenerating` backed by Apple's Foundation Models. -/// Runs entirely on device (no network, no data leaves the phone), which suits -/// summarizing location history over the selected window. Reports an -/// `ActivitySummaryUnavailableError` when the system model can't run so the UI -/// can guide the user rather than showing a generic failure. -public struct FoundationModelSummaryGenerator: ActivitySummaryGenerating { - public init() {} - - public func summarize(_ input: RecentActivityInput) async throws -> String { - switch SystemLanguageModel.default.availability { - case .available: - break - case let .unavailable(reason): - throw ActivitySummaryUnavailableError(reason: Self.map(reason)) - } - - let session = LanguageModelSession(instructions: Self.instructions) - let response = try await session.respond(to: Self.prompt(for: input)) - return response.content.trimmingCharacters(in: .whitespacesAndNewlines) - } - - private static let instructions = """ - You summarize a person's recent location history for a residency/day-count \ - audit log. You are given timestamped GPS readings and the tracked region \ - each reading falls in. Write a concise, factual, 2-3 sentence summary of \ - where the person appears to have been over the period. Mention the main \ - regions and the rough times or transitions between them. Do not invent \ - places or motives, do not give advice, and do not add a preamble — return \ - only the summary. - """ - - /// Render the structured window into a compact prompt the model can read. - /// Times use the current locale/time zone so the summary reads naturally, - /// and the covered period is described from the interval rather than a fixed - /// "last 24 hours" so it stays honest across the selectable windows. - private static func prompt(for input: RecentActivityInput) -> String { - let readings = input.segments - .map { segment in - let coordinate = - "\(segment.coordinate.latitude.formatted(.number.precision(.fractionLength(4)))), " + - "\(segment.coordinate.longitude.formatted(.number.precision(.fractionLength(4))))" - return "- \(timeSpan(for: segment)): \(segment.region.localizedName) (\(coordinate))" - } - .joined(separator: "\n") - let period = periodPhrase(for: input.interval) - - return """ - These are the regions the device was in from \(period), oldest first, \ - each with the time span it covers: - \(readings) - - Summarize where the person was over this period. - """ - } - - /// A single reading renders as one time; a multi-reading stay renders as a - /// "start – end" span so the model can convey how long it lasted. - private static func timeSpan(for segment: RecentActivitySegment) -> String { - let start = segment.start.formatted(date: .abbreviated, time: .shortened) - guard segment.end > segment.start else { return start } - let end = segment.end.formatted(date: .abbreviated, time: .shortened) - return "\(start) – \(end)" - } - - /// A compact " to " phrase for the covered interval. Not - /// user-facing UI copy — it's part of the model prompt — so it stays inline - /// like the rest of the prompt rather than routing through a catalog. - private static func periodPhrase(for interval: DateInterval) -> String { - let start = interval.start.formatted(date: .abbreviated, time: .shortened) - let end = interval.end.formatted(date: .abbreviated, time: .shortened) - return "\(start) to \(end)" - } - - private static func map( - _ reason: SystemLanguageModel.Availability.UnavailableReason, - ) -> ActivitySummaryUnavailableReason { - switch reason { - case .deviceNotEligible: .deviceNotEligible - case .appleIntelligenceNotEnabled: .appleIntelligenceNotEnabled - case .modelNotReady: .modelNotReady - @unknown default: .unknown - } - } -} diff --git a/Where/WhereCore/Sources/RecentActivity/RecentActivitySummarizer.swift b/Where/WhereCore/Sources/RecentActivity/RecentActivitySummarizer.swift deleted file mode 100644 index b5e52b156..000000000 --- a/Where/WhereCore/Sources/RecentActivity/RecentActivitySummarizer.swift +++ /dev/null @@ -1,195 +0,0 @@ -import Foundation -import PeriscopeCore -import RegionKit - -/// One attributed reading in a recent-activity window: when the device was -/// somewhere, which tracked region that coordinate fell in, and the raw -/// coordinate. The raw unit the summarizer collapses into -/// `RecentActivitySegment`s before handing them to the generator. -public struct RecentActivityStop: Hashable, Sendable { - public let timestamp: Date - public let region: Region - public let coordinate: Coordinate - - public init(timestamp: Date, region: Region, coordinate: Coordinate) { - self.timestamp = timestamp - self.region = region - self.coordinate = coordinate - } -} - -/// A contiguous stretch the device spent in one region: the region, the first -/// (`start`) and last (`end`) reading times of the run, and a representative -/// coordinate (the arrival reading's). Produced by collapsing consecutive -/// same-region readings so the generator sees dwell spans — how long each stay -/// lasted — rather than every ping. `start == end` for a single-reading run. -public struct RecentActivitySegment: Hashable, Sendable { - public let region: Region - public let start: Date - public let end: Date - public let coordinate: Coordinate - - public init(region: Region, start: Date, end: Date, coordinate: Coordinate) { - self.region = region - self.start = start - self.end = end - self.coordinate = coordinate - } -} - -/// The structured input a `ActivitySummaryGenerating` turns into prose: the -/// window it covers plus the region segments within it, oldest first. -public struct RecentActivityInput: Hashable, Sendable { - public let interval: DateInterval - public let segments: [RecentActivitySegment] - - public init(interval: DateInterval, segments: [RecentActivitySegment]) { - self.interval = interval - self.segments = segments - } -} - -/// The outcome of a recent-activity summary. Distinguishes a real generated -/// summary from an empty window so the UI can show a distinct "nothing tracked" -/// state rather than an empty string that reads like a failure. -public enum RecentActivitySummary: Sendable, Equatable { - case summary(String) - case empty -} - -/// Why an on-device summary can't be produced right now. Mirrors the reasons -/// the system language model reports so the UI can guide the user (e.g. enable -/// Apple Intelligence) instead of showing a generic error. -public enum ActivitySummaryUnavailableReason: Hashable, Sendable { - case deviceNotEligible - case appleIntelligenceNotEnabled - case modelNotReady - case unknown -} - -/// Thrown by a generator when the on-device model is unavailable. A typed error -/// (not a benign default) so the summary surfaces an honest, actionable state. -public struct ActivitySummaryUnavailableError: Error, Hashable, Sendable { - public let reason: ActivitySummaryUnavailableReason - - public init(reason: ActivitySummaryUnavailableReason) { - self.reason = reason - } -} - -/// Seam over the text generator so `RecentActivitySummarizer` can be unit-tested -/// with a stub while production wires the on-device Foundation Models generator. -/// Implementations throw `ActivitySummaryUnavailableError` when the model can't -/// run and rethrow any generation failure — never a silent empty summary. -public protocol ActivitySummaryGenerating: Sendable { - func summarize(_ input: RecentActivityInput) async throws -> String -} - -/// Produces a natural-language summary of a look-back window of tracked -/// locations using an on-device language model. Reads the raw samples in the -/// window, attributes each to a `Region`, condenses consecutive same-region -/// readings into dwell segments, and hands the structured input to an injected -/// generator. Failures (an unavailable model, a generation error) propagate so -/// the caller can surface an honest state. -public actor RecentActivitySummarizer { - /// Upper bound on the number of region segments handed to the generator. - /// Long windows (e.g. the year so far) can hold thousands of readings; - /// keeping only the most recent segments bounds the prompt so it fits the - /// on-device model's context. - public static let defaultSegmentLimit = 60 - - private let store: any WhereStore - private let attributor: any RegionAttributing - private let generator: any ActivitySummaryGenerating - private let calendar: Calendar - private let now: @Sendable () -> Date - private let segmentLimit: Int - private var history: LocationHistoryReader { - LocationHistoryReader(store: store) - } - - private static let logger = WhereLog.recentActivity(RecentActivitySummarizerLog.self) - - init( - store: any WhereStore, - attributor: any RegionAttributing, - generator: any ActivitySummaryGenerating, - calendar: Calendar, - now: @escaping @Sendable () -> Date, - segmentLimit: Int, - ) { - self.store = store - self.attributor = attributor - self.generator = generator - self.calendar = calendar - self.now = now - self.segmentLimit = segmentLimit - } - - /// Summarize the tracked locations in `window`. Returns `.empty` when - /// nothing was recorded in the window; otherwise attributes each sample to a - /// region, condenses consecutive same-region readings into dwell segments, - /// and asks the generator for prose. Throws on read failure, an unavailable - /// model, or a generation error. - public func summary(for window: RecentActivityWindow) async throws -> RecentActivitySummary { - let interval = window.interval(now: now(), calendar: calendar) - let samples = try await history.samples(in: interval) - guard !samples.isEmpty else { - Self.logger { .skippedNoSamples } - return .empty - } - let segments = Self.logger.measure(.attribute, budget: .seconds(1)) { - let stops = samples.map { sample in - RecentActivityStop( - timestamp: sample.timestamp, - region: attributor.region(at: sample.coordinate), - coordinate: sample.coordinate, - ) - } - return Self.segments(from: stops, limit: segmentLimit) - } - let text = try await Self.logger.measure(.generate) { - try await generator.summarize( - RecentActivityInput(interval: interval, segments: segments), - ) - } - Self.logger { .generated(segmentCount: segments.count) } - return .summary(text) - } - - /// Collapse consecutive readings attributed to the same region into one - /// segment spanning the run's first-to-last reading, then keep only the most - /// recent `limit` segments. The run-length collapse preserves dwell time - /// (how long each stay lasted) while the hard cap keeps a long, fast-moving - /// window within the prompt budget. Input is assumed oldest-first (the store - /// sorts ascending). - private static func segments( - from stops: [RecentActivityStop], - limit: Int, - ) -> [RecentActivitySegment] { - precondition(limit > 0, "The segment limit must be positive.") - var segments: [RecentActivitySegment] = [] - for stop in stops { - let lastIndex = segments.count - 1 - if lastIndex >= 0, segments[lastIndex].region == stop.region { - // Same region as the run in progress: extend its end time, - // keeping the arrival time and coordinate. - let run = segments[lastIndex] - segments[lastIndex] = RecentActivitySegment( - region: run.region, - start: run.start, - end: stop.timestamp, - coordinate: run.coordinate, - ) - } else { - segments.append(RecentActivitySegment( - region: stop.region, - start: stop.timestamp, - end: stop.timestamp, - coordinate: stop.coordinate, - )) - } - } - return segments.count > limit ? Array(segments.suffix(limit)) : segments - } -} diff --git a/Where/WhereCore/Sources/RecentActivity/RecentActivityWindow.swift b/Where/WhereCore/Sources/RecentActivity/RecentActivityWindow.swift deleted file mode 100644 index 253281363..000000000 --- a/Where/WhereCore/Sources/RecentActivity/RecentActivityWindow.swift +++ /dev/null @@ -1,46 +0,0 @@ -import Foundation - -/// The look-back range a recent-activity summary covers. `day`, `week`, and -/// `month` are rolling look-backs ending at "now"; `yearToDate` runs from the -/// start of the current calendar year to now. Modeled as a typed, exhaustive -/// enum (not a raw `TimeInterval`) so the calendar-relative "year so far" -/// window can't be confused with a fixed span and every window is enumerable -/// for a picker. -public enum RecentActivityWindow: Sendable, Hashable, CaseIterable { - case day - case week - case month - case yearToDate - - /// The date interval this window covers, ending at `now`. Rolling windows - /// subtract a fixed span; `yearToDate` starts at the first instant of - /// `now`'s calendar year. The interval is always non-empty for `yearToDate` - /// (year start never follows `now`). - public func interval(now: Date, calendar: Calendar) -> DateInterval { - switch self { - case .day: - DateInterval(start: now.addingTimeInterval(-Self.secondsPerDay), end: now) - case .week: - DateInterval(start: now.addingTimeInterval(-7 * Self.secondsPerDay), end: now) - case .month: - DateInterval(start: now.addingTimeInterval(-30 * Self.secondsPerDay), end: now) - case .yearToDate: - DateInterval(start: Self.startOfYear(for: now, calendar: calendar), end: now) - } - } - - private static let secondsPerDay: TimeInterval = 24 * 60 * 60 - - /// First instant of `now`'s calendar year. A calendar that can't resolve a - /// year from a date is a misconfiguration, not a user failure, so debug - /// traps and release falls back to `now` (a zero-length window that reads - /// as "nothing tracked" rather than crashing shipping code). - private static func startOfYear(for now: Date, calendar: Calendar) -> Date { - let components = calendar.dateComponents([.year], from: now) - guard let start = calendar.date(from: components) else { - assertionFailure("Calendar could not resolve the start of year for \(now)") - return now - } - return start - } -} diff --git a/Where/WhereCore/Sources/WhereServices.swift b/Where/WhereCore/Sources/WhereServices.swift index aff11e36c..9e738701e 100644 --- a/Where/WhereCore/Sources/WhereServices.swift +++ b/Where/WhereCore/Sources/WhereServices.swift @@ -60,11 +60,6 @@ public struct WhereServices: Sendable { public let backup: BackupCoordinator /// Data-quality issue detection for the Resolve tab. public let resolution: DataIssueScanner - /// On-device summary of a selectable look-back window of tracked locations - /// (see `RecentActivityWindow`). Named distinctly from `summary` (the daily - /// notification recap) — this one is an on-demand Foundation Models - /// narrative. - public let recentActivity: RecentActivitySummarizer /// The persistence boundary, retained so `dataChangeUpdates()` can hand out /// the store's `changes()` stream — the single read-refresh signal every /// write origin (manual edit, live GPS, remote sync) funnels through. @@ -122,7 +117,6 @@ public struct WhereServices: Sendable { locationOutbox: any LocationOutbox = NoOpLocationOutbox(), importRecoveryPersistence: any BackupImportRecoveryPersisting = NoopBackupImportRecoveryPersistence(), - activitySummaryGenerator: any ActivitySummaryGenerating = FoundationModelSummaryGenerator(), now: @escaping @Sendable () -> Date = { Date() }, ) { let currentDevice = installationContext.currentDevice @@ -267,15 +261,6 @@ public struct WhereServices: Sendable { changes: store.remoteChanges(), reconcile: { await derivedData.reconcile() }, ) - let recentActivity = RecentActivitySummarizer( - store: store, - attributor: attributor, - generator: activitySummaryGenerator, - calendar: aggregator.calendar, - now: now, - segmentLimit: RecentActivitySummarizer.defaultSegmentLimit, - ) - self.reports = reports self.evidence = evidence self.reminders = reminders @@ -287,7 +272,6 @@ public struct WhereServices: Sendable { self.journal = journal self.backup = backup self.resolution = resolution - self.recentActivity = recentActivity self.store = store self.attributor = attributor self.aggregator = aggregator @@ -321,7 +305,6 @@ public struct WhereServices: Sendable { widgetRefresher: any WidgetTimelineRefreshing, locationOutbox: any LocationOutbox = NoOpLocationOutbox(), importRecoveryPersistence: any BackupImportRecoveryPersisting, - activitySummaryGenerator: any ActivitySummaryGenerating = FoundationModelSummaryGenerator(), now: @escaping @Sendable () -> Date = { Date() }, ) async throws -> WhereServices { let tracked = try await store.trackedRegions() @@ -345,7 +328,6 @@ public struct WhereServices: Sendable { widgetRefresher: widgetRefresher, locationOutbox: locationOutbox, importRecoveryPersistence: importRecoveryPersistence, - activitySummaryGenerator: activitySummaryGenerator, now: now, ) } diff --git a/Where/WhereCore/Tests/Logging/WhereLogTests.swift b/Where/WhereCore/Tests/Logging/WhereLogTests.swift index 5f22915ae..625b3587e 100644 --- a/Where/WhereCore/Tests/Logging/WhereLogTests.swift +++ b/Where/WhereCore/Tests/Logging/WhereLogTests.swift @@ -21,7 +21,6 @@ struct WhereLogTreeTests { #expect(WhereLog.widgets.primaryScope.name == "widgets") #expect(WhereLog.session.primaryScope.name == "session") #expect(WhereLog.evidence.primaryScope.name == "evidence") - #expect(WhereLog.recentActivity.primaryScope.name == "recentActivity") #expect(WhereLog.reporting.primaryScope.name == "reporting") #expect(WhereLog.reporting.primaryScope.parentID == rootID) } diff --git a/Where/WhereCore/Tests/RecentActivitySummarizerTests.swift b/Where/WhereCore/Tests/RecentActivitySummarizerTests.swift deleted file mode 100644 index 13966a84f..000000000 --- a/Where/WhereCore/Tests/RecentActivitySummarizerTests.swift +++ /dev/null @@ -1,199 +0,0 @@ -import Foundation -import RegionKit -import Testing -@testable import WhereCore - -/// Covers windowing (rolling and wider ranges), region attribution, the -/// collapse-and-cap of readings into dwell segments, empty-window handling, and -/// error propagation of `RecentActivitySummarizer` against a scripted generator -/// (the on-device Foundation Models path is device-only and not unit-tested). -struct RecentActivitySummarizerTests { - private enum StubError: Error { case boom } - - /// Scripted `ActivitySummaryGenerating` that records the input it was handed - /// and returns a canned outcome, so tests can assert both the summary and - /// what the summarizer fed the model. - private actor StubGenerator: ActivitySummaryGenerating { - enum Outcome { - case text(String) - case unavailable(ActivitySummaryUnavailableReason) - case failure - } - - private let outcome: Outcome - private(set) var receivedInput: RecentActivityInput? - - init(_ outcome: Outcome) { - self.outcome = outcome - } - - func summarize(_ input: RecentActivityInput) async throws -> String { - receivedInput = input - switch outcome { - case let .text(text): return text - case let .unavailable(reason): throw ActivitySummaryUnavailableError(reason: reason) - case .failure: throw StubError.boom - } - } - } - - private static let now = WhereCoreTestSupport.iso("2026-05-02T12:00:00-07:00") - - private static func makeSummarizer( - store: SwiftDataStore, - generator: StubGenerator, - segmentLimit: Int = RecentActivitySummarizer.defaultSegmentLimit, - ) -> RecentActivitySummarizer { - RecentActivitySummarizer( - store: store, - attributor: RegionAttributor.shared, - generator: generator, - calendar: .current, - now: { now }, - segmentLimit: segmentLimit, - ) - } - - private static func californiaSample(at date: Date) -> LocationSample { - sample(at: date, latitude: 37.7749, longitude: -122.4194) - } - - private static func newYorkSample(at date: Date) -> LocationSample { - sample(at: date, latitude: 40.7128, longitude: -74.0060) - } - - private static func sample( - at date: Date, - latitude: Double, - longitude: Double, - ) -> LocationSample { - LocationSample( - timestamp: date, - coordinate: Coordinate(latitude: latitude, longitude: longitude), - horizontalAccuracy: 5, - source: .gpsSignificantChange, - ) - } - - @Test func emptyWindowReturnsEmptyWithoutCallingGenerator() async throws { - let store = try SwiftDataStore.inMemory() - let generator = StubGenerator(.text("unused")) - let summarizer = Self.makeSummarizer(store: store, generator: generator) - - #expect(try await summarizer.summary(for: .day) == .empty) - #expect(await generator.receivedInput == nil) - } - - @Test func summarizesOnlyReadingsInsideThe24hWindow() async throws { - let store = try SwiftDataStore.inMemory() - // One reading an hour ago (in window) and one 30 hours ago (out of it). - let inWindow = Self.californiaSample(at: Self.now.addingTimeInterval(-60 * 60)) - let outOfWindow = Self.californiaSample(at: Self.now.addingTimeInterval(-30 * 60 * 60)) - try await store.perform { - try await store.add(sample: inWindow) - try await store.add(sample: outOfWindow) - } - let generator = StubGenerator(.text("You were in California.")) - let summarizer = Self.makeSummarizer(store: store, generator: generator) - - #expect(try await summarizer.summary(for: .day) == .summary("You were in California.")) - let input = try #require(await generator.receivedInput) - #expect(input.segments.count == 1) - #expect(input.segments.first?.region == .california) - // A lone reading is a zero-length segment at its own timestamp. - #expect(input.segments.first?.start == inWindow.timestamp) - #expect(input.segments.first?.end == inWindow.timestamp) - } - - @Test func unavailableModelPropagatesTypedError() async throws { - let store = try SwiftDataStore.inMemory() - try await store.perform { - try await store - .add(sample: Self.californiaSample(at: Self.now.addingTimeInterval(-3600))) - } - let generator = StubGenerator(.unavailable(.appleIntelligenceNotEnabled)) - let summarizer = Self.makeSummarizer(store: store, generator: generator) - - await #expect( - throws: ActivitySummaryUnavailableError(reason: .appleIntelligenceNotEnabled), - ) { - _ = try await summarizer.summary(for: .day) - } - } - - @Test func generatorFailurePropagates() async throws { - let store = try SwiftDataStore.inMemory() - try await store.perform { - try await store - .add(sample: Self.californiaSample(at: Self.now.addingTimeInterval(-3600))) - } - let generator = StubGenerator(.failure) - let summarizer = Self.makeSummarizer(store: store, generator: generator) - - await #expect(throws: StubError.self) { - _ = try await summarizer.summary(for: .day) - } - } - - @Test func collapsesConsecutiveSameRegionReadingsToOneSegment() async throws { - let store = try SwiftDataStore.inMemory() - // Three readings in the same region within the window collapse to one - // segment spanning the run's first to last reading (its dwell time). - try await store.perform { - try await store - .add(sample: Self.californiaSample(at: Self.now.addingTimeInterval(-3 * 3600))) - try await store - .add(sample: Self.californiaSample(at: Self.now.addingTimeInterval(-2 * 3600))) - try await store - .add(sample: Self.californiaSample(at: Self.now.addingTimeInterval(-3600))) - } - let generator = StubGenerator(.text("summary")) - let summarizer = Self.makeSummarizer(store: store, generator: generator) - - _ = try await summarizer.summary(for: .day) - let input = try #require(await generator.receivedInput) - #expect(input.segments.count == 1) - #expect(input.segments.first?.start == Self.now.addingTimeInterval(-3 * 3600)) - #expect(input.segments.first?.end == Self.now.addingTimeInterval(-3600)) - } - - @Test func capsSegmentsToTheMostRecentWithinTheLimit() async throws { - let store = try SwiftDataStore.inMemory() - // Five alternating-region readings (so none collapse); a limit of 3 - // keeps only the three most recent segments. - try await store.perform { - try await store - .add(sample: Self.californiaSample(at: Self.now.addingTimeInterval(-5 * 3600))) - try await store - .add(sample: Self.newYorkSample(at: Self.now.addingTimeInterval(-4 * 3600))) - try await store - .add(sample: Self.californiaSample(at: Self.now.addingTimeInterval(-3 * 3600))) - try await store - .add(sample: Self.newYorkSample(at: Self.now.addingTimeInterval(-2 * 3600))) - try await store - .add(sample: Self.californiaSample(at: Self.now.addingTimeInterval(-3600))) - } - let generator = StubGenerator(.text("summary")) - let summarizer = Self.makeSummarizer(store: store, generator: generator, segmentLimit: 3) - - _ = try await summarizer.summary(for: .day) - let input = try #require(await generator.receivedInput) - #expect(input.segments.count == 3) - #expect(input.segments.first?.start == Self.now.addingTimeInterval(-3 * 3600)) - #expect(input.segments.last?.start == Self.now.addingTimeInterval(-3600)) - } - - @Test func widerWindowIncludesReadingsOutsideThe24hWindow() async throws { - let store = try SwiftDataStore.inMemory() - // A reading three days ago is outside `.day` but inside `.week`. - try await store.perform { - try await store - .add(sample: Self.californiaSample(at: Self.now.addingTimeInterval(-3 * 24 * 3600))) - } - let generator = StubGenerator(.text("You were in California.")) - let summarizer = Self.makeSummarizer(store: store, generator: generator) - - #expect(try await summarizer.summary(for: .day) == .empty) - #expect(try await summarizer.summary(for: .week) == .summary("You were in California.")) - } -} diff --git a/Where/WhereCore/Tests/RecentActivityWindowTests.swift b/Where/WhereCore/Tests/RecentActivityWindowTests.swift deleted file mode 100644 index f850025b5..000000000 --- a/Where/WhereCore/Tests/RecentActivityWindowTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -import Foundation -import Testing -@testable import WhereCore - -/// Covers `RecentActivityWindow`'s interval math: the rolling look-backs and -/// the calendar-relative "year so far", using a fixed calendar/time zone so the -/// year boundary is deterministic regardless of where the test runs. -struct RecentActivityWindowTests { - private static let now = WhereCoreTestSupport.iso("2026-05-02T12:00:00+00:00") - - private static let calendar: Calendar = { - var calendar = Calendar(identifier: .gregorian) - calendar.timeZone = TimeZone(identifier: "UTC")! - return calendar - }() - - private static let day: TimeInterval = 24 * 60 * 60 - - @Test func dayIsARollingTwentyFourHours() { - let interval = RecentActivityWindow.day.interval(now: Self.now, calendar: Self.calendar) - #expect(interval.end == Self.now) - #expect(interval.start == Self.now.addingTimeInterval(-Self.day)) - } - - @Test func weekIsARollingSevenDays() { - let interval = RecentActivityWindow.week.interval(now: Self.now, calendar: Self.calendar) - #expect(interval.end == Self.now) - #expect(interval.start == Self.now.addingTimeInterval(-7 * Self.day)) - } - - @Test func monthIsARollingThirtyDays() { - let interval = RecentActivityWindow.month.interval(now: Self.now, calendar: Self.calendar) - #expect(interval.end == Self.now) - #expect(interval.start == Self.now.addingTimeInterval(-30 * Self.day)) - } - - @Test func yearToDateStartsAtTheStartOfTheCalendarYear() { - let interval = RecentActivityWindow.yearToDate - .interval(now: Self.now, calendar: Self.calendar) - #expect(interval.end == Self.now) - #expect(interval.start == WhereCoreTestSupport.iso("2026-01-01T00:00:00+00:00")) - } -} diff --git a/Where/WhereIntents/README.md b/Where/WhereIntents/README.md index 5b38af6be..514a4df86 100644 --- a/Where/WhereIntents/README.md +++ b/Where/WhereIntents/README.md @@ -12,8 +12,7 @@ installs a stack built with over the same `SwiftDataStore` it opened, and an intent that fires earlier waits for that install rather than opening its own store; no GPS started via `WhereCore`'s `IdleLocationSource`), do their read/write through the existing -collaborators -(`reports`, `recentActivity`, `journal`) using a Gregorian calendar +collaborators (`reports`, `journal`) using a Gregorian calendar (`Calendar.whereIntents`, matching the domain's aggregation so year/day math lines up), and render with [`WhereUI`](../WhereUI/) snippet views. The `AppShortcutsProvider` that gives Siri its spoken phrases lives in the **Where** @@ -29,7 +28,6 @@ always discovers it. | `DaysInRegionIntent` | "How many days in California this year?" | `ReportReader.yearReport` → `totals[region]` | | `RegionOnDateIntent` | "What region was I in on June 3?" | `yearReport.days` for that day | | `TodayRegionsIntent` | "Where am I today?" | `WidgetSnapshotStore` fast path, `yearReport` fallback | -| `RecentActivitySummaryIntent` | "Summarize where I've been this week." | `RecentActivitySummarizer.summary(for:)` | ### Action (write) @@ -67,10 +65,6 @@ its day-count query. (so "days in Texas" answers even when untracked), while `suggestedEntities()` and the Spotlight index surface the user's **tracked** set (via `WhereServices.trackedRegions()`). -- `ActivityWindowAppEnum` — mirrors `RecentActivityWindow` (24h / week / month / - year so far). An enum is fine here because these display names have no - RegionKit-owned source. - ## Timing Every intent's work is one budgeted Periscope span named after the intent diff --git a/Where/WhereIntents/Sources/ActivityWindowAppEnum.swift b/Where/WhereIntents/Sources/ActivityWindowAppEnum.swift deleted file mode 100644 index 7638087bb..000000000 --- a/Where/WhereIntents/Sources/ActivityWindowAppEnum.swift +++ /dev/null @@ -1,42 +0,0 @@ -import AppIntents -import WhereCore - -/// The look-back window for the recent-activity summary intent, mirroring -/// `WhereCore.RecentActivityWindow` (rolling 24h / week / month, plus year so -/// far) as a Siri-resolvable menu. Round-trips losslessly with the domain enum. -public enum ActivityWindowAppEnum: String, AppEnum, CaseIterable { - case day - case week - case month - case yearToDate - - /// App Intents extracts this static metadata at build time, so both must be - /// compile-time-constant literals (the framework localizes them through the - /// app's App Intents string extraction, not this module's catalog). - public static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Time Range") - - public static let caseDisplayRepresentations: [ActivityWindowAppEnum: DisplayRepresentation] = [ - .day: DisplayRepresentation(title: "Last 24 Hours"), - .week: DisplayRepresentation(title: "Past Week"), - .month: DisplayRepresentation(title: "Past Month"), - .yearToDate: DisplayRepresentation(title: "Year So Far"), - ] - - public init(_ window: RecentActivityWindow) { - switch window { - case .day: self = .day - case .week: self = .week - case .month: self = .month - case .yearToDate: self = .yearToDate - } - } - - public var window: RecentActivityWindow { - switch self { - case .day: .day - case .week: .week - case .month: .month - case .yearToDate: .yearToDate - } - } -} diff --git a/Where/WhereIntents/Sources/IntentStrings.swift b/Where/WhereIntents/Sources/IntentStrings.swift index 20cca694d..000157e68 100644 --- a/Where/WhereIntents/Sources/IntentStrings.swift +++ b/Where/WhereIntents/Sources/IntentStrings.swift @@ -44,33 +44,6 @@ enum IntentStrings { return String(localized: .dialogTodaySome(regionList(regions))) } - // MARK: Recent activity - - static func recentActivity( - _ summary: RecentActivitySummary, - window: RecentActivityWindow, - ) -> String { - switch summary { - case let .summary(text): - text - case .empty: - String(localized: .dialogRecentActivityEmpty(windowPhrase(window))) - } - } - - static func recentActivityUnavailable(_ reason: ActivitySummaryUnavailableReason) -> String { - switch reason { - case .deviceNotEligible: - String(localized: .dialogRecentActivityUnavailableDeviceNotEligible) - case .appleIntelligenceNotEnabled: - String(localized: .dialogRecentActivityUnavailableAppleIntelligenceNotEnabled) - case .modelNotReady: - String(localized: .dialogRecentActivityUnavailableModelNotReady) - case .unknown: - String(localized: .dialogRecentActivityUnavailableUnknown) - } - } - // MARK: Logging (action intents) /// The note stamped on a manual entry made from an intent. Persisted with @@ -119,17 +92,6 @@ enum IntentStrings { .formatted(.list(type: .and)) } - /// A natural-language phrase for a recent-activity window, for the empty - /// dialog ("Nothing was tracked in the past week."). - private static func windowPhrase(_ window: RecentActivityWindow) -> String { - switch window { - case .day: String(localized: .dialogWindowDay) - case .week: String(localized: .dialogWindowWeek) - case .month: String(localized: .dialogWindowMonth) - case .yearToDate: String(localized: .dialogWindowYearToDate) - } - } - /// Year without a grouping separator ("2026", not "2,026") — matching how /// WhereUI renders years. private static func yearText(_ year: Int) -> String { diff --git a/Where/WhereIntents/Sources/Logging/WhereIntentsLog.swift b/Where/WhereIntents/Sources/Logging/WhereIntentsLog.swift index d1aab52ff..2c828c2c8 100644 --- a/Where/WhereIntents/Sources/Logging/WhereIntentsLog.swift +++ b/Where/WhereIntents/Sources/Logging/WhereIntentsLog.swift @@ -1,8 +1,7 @@ import PeriscopeCore import WhereCore -/// Structured events for the Where App Intents surface — Spotlight indexing of -/// the tracked regions and the recent-activity summary intent. These run in the +/// Structured events for the Where App Intents surface. These run in the /// app/intents process, which keeps `Periscope.shared` OSLog-only (no /// persistent store of its own). enum WhereIntentsLog: LogEvent { @@ -39,23 +38,19 @@ enum WhereIntentsLog: LogEvent { case daysInRegionSnippet = "days-in-region-snippet" case logDay = "log-day" case logTrip = "log-trip" - case recentActivitySummary = "recent-activity-summary" case regionOnDate = "region-on-date" case todayRegions = "today-regions" /// Siri and Shortcuts hold the user waiting on `perform()`, so these are /// tight: a single year's aggregated read or a one-day write should be - /// well under a second. The exceptions earn their slack — a trip - /// backfills a range in one transaction, and the recent-activity summary - /// waits on an on-device language model that may still be warming. + /// well under a second. A trip earns more slack because it backfills a + /// range in one transaction. var budget: Duration { switch self { case .daysInRegion, .daysInRegionSnippet, .logDay, .regionOnDate, .todayRegions: .seconds(2) case .logTrip: .seconds(5) - case .recentActivitySummary: - .seconds(15) } } } @@ -65,17 +60,13 @@ enum WhereIntentsLog: LogEvent { /// Indexing the tracked regions into Spotlight failed /// (degraded-but-handled: search integration is a nicety). case spotlightIndexFailed(description: String) - /// The recent-activity summary couldn't be produced (e.g. Apple - /// Intelligence is off or the model is warming). - case recentActivityUnavailable(reason: String) - static let eventName = "WhereIntents" var level: LogLevel { switch self { case .spotlightIndexed: .info - case .spotlightIndexFailed, .recentActivityUnavailable: + case .spotlightIndexFailed: .warning } } @@ -86,8 +77,6 @@ enum WhereIntentsLog: LogEvent { "Indexed \(regionCount) region(s) for Spotlight" case let .spotlightIndexFailed(description): "Failed to index regions for Spotlight: \(description)" - case let .recentActivityUnavailable(reason): - "Recent-activity summary unavailable: \(reason)" } } } diff --git a/Where/WhereIntents/Sources/RecentActivitySummaryIntent.swift b/Where/WhereIntents/Sources/RecentActivitySummaryIntent.swift deleted file mode 100644 index 297f8446c..000000000 --- a/Where/WhereIntents/Sources/RecentActivitySummaryIntent.swift +++ /dev/null @@ -1,53 +0,0 @@ -import AppIntents -import Foundation -import PeriscopeCore -import WhereCore - -/// "Summarize where I've been this week." — the on-device Foundation Models -/// narrative of a look-back window. When the model is unavailable the intent -/// speaks the actionable reason (e.g. turn on Apple Intelligence) rather than a -/// generic failure, and logs it. -public struct RecentActivitySummaryIntent: AppIntent { - public static let title: LocalizedStringResource = "Summarize Recent Activity" - - public static let description = IntentDescription( - "Get an on-device summary of where you've been over a recent window.", - ) - - @Parameter(title: "Time Range") - public var window: ActivityWindowAppEnum - - /// The app-registered services handoff (see `IntentServices`); resolved by - /// the App Intents dependency container, never a singleton of ours. - @Dependency private var intentServices: IntentServices - - public init() {} - - public init(window: ActivityWindowAppEnum) { - self.window = window - } - - public func perform() async throws -> some IntentResult & ProvidesDialog { - let services = try await intentServices.current() - let reader = WhereIntentReader(services: services) - do { - let summary = try await measureIntent(.recentActivitySummary) { - try await reader.recentActivity(window.window) - } - return .result( - dialog: IntentDialog( - "\(IntentStrings.recentActivity(summary, window: window.window))", - ), - ) - } catch let error as ActivitySummaryUnavailableError { - // User-recoverable (Apple Intelligence off, model warming): surface - // the reason in the dialog and log it — never a silent empty result. - WhereIntentsLog.logger { - .recentActivityUnavailable(reason: String(describing: error.reason)) - } - return .result( - dialog: IntentDialog("\(IntentStrings.recentActivityUnavailable(error.reason))"), - ) - } - } -} diff --git a/Where/WhereIntents/Sources/Resources/Localizable.xcstrings b/Where/WhereIntents/Sources/Resources/Localizable.xcstrings index 8a8be3f34..e401d15cb 100644 --- a/Where/WhereIntents/Sources/Resources/Localizable.xcstrings +++ b/Where/WhereIntents/Sources/Resources/Localizable.xcstrings @@ -37,14 +37,6 @@ "comment" : "Description of the \"Count Days in a Region\" intent.", "isCommentAutoGenerated" : true }, - "Get an on-device summary of where you've been over a recent window." : { - "comment" : "Description of the intent to get an on-device summary of where the user has been over a recent window.", - "isCommentAutoGenerated" : true - }, - "Last 24 Hours" : { - "comment" : "Display name for the \"Last 24 Hours\" option in the Siri-resolved menu.", - "isCommentAutoGenerated" : true - }, "Log a Day's Regions" : { "comment" : "Intent title.", "isCommentAutoGenerated" : true @@ -56,14 +48,6 @@ "comment" : "Description of the \"Find Regions on a Date\" intent.", "isCommentAutoGenerated" : true }, - "Past Month" : { - "comment" : "Display name for the past month option in the recent-activity summary intent.", - "isCommentAutoGenerated" : true - }, - "Past Week" : { - "comment" : "Displayed title for the \"Past Week\" option in the Siri-resolved menu.", - "isCommentAutoGenerated" : true - }, "Record which regions you were in on a day." : { "comment" : "Description of the \"Log a Day's Regions\" intent.", "isCommentAutoGenerated" : true @@ -86,22 +70,11 @@ }, "Start Date" : { - }, - "Summarize Recent Activity" : { - "comment" : "Intent to summarize recent activity.", - "isCommentAutoGenerated" : true - }, - "Time Range" : { - }, "Year" : { "comment" : "Label for the year parameter in the \"Count Days in a Region\" intent.", "isCommentAutoGenerated" : true }, - "Year So Far" : { - "comment" : "Display name for the \"Year So Far\" option in the", - "isCommentAutoGenerated" : true - }, "audit.note.siri" : { "comment" : "Note stored on a manual day entry made through an intent.", "extractionState" : "manual", @@ -208,62 +181,6 @@ } } }, - "dialog.recentActivity.empty" : { - "comment" : "Spoken result when a window has no tracked activity. %1$@ window phrase.", - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nothing was tracked in %1$@." - } - } - } - }, - "dialog.recentActivity.unavailable.appleIntelligenceNotEnabled" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Turn on Apple Intelligence in Settings to summarize where you've been." - } - } - } - }, - "dialog.recentActivity.unavailable.deviceNotEligible" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "This device doesn't support on-device summaries." - } - } - } - }, - "dialog.recentActivity.unavailable.modelNotReady" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The on-device model is still getting ready. Try again shortly." - } - } - } - }, - "dialog.recentActivity.unavailable.unknown" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "On-device summaries aren't available right now." - } - } - } - }, "dialog.regionOnDate.none" : { "comment" : "Spoken result when a date has nothing logged. %1$@ formatted date.", "extractionState" : "manual", @@ -311,50 +228,6 @@ } } }, - "dialog.window.day" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "the last 24 hours" - } - } - } - }, - "dialog.window.month" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "the past month" - } - } - } - }, - "dialog.window.week" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "the past week" - } - } - } - }, - "dialog.window.yearToDate" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "the year so far" - } - } - } - }, "snippet.logTodayHere" : { "comment" : "Button in the day-count snippet that logs today for the shown region.", "extractionState" : "manual", diff --git a/Where/WhereIntents/Sources/WhereIntentReader.swift b/Where/WhereIntents/Sources/WhereIntentReader.swift index ed7f75a19..1f8d992ff 100644 --- a/Where/WhereIntents/Sources/WhereIntentReader.swift +++ b/Where/WhereIntents/Sources/WhereIntentReader.swift @@ -4,9 +4,8 @@ import WhereCore /// The read half of the App Intents layer: turns a `WhereServices` into the /// answers the query intents speak. Pure orchestration over the existing -/// `reports` / `recentActivity` collaborators (and the published widget -/// snapshot) — no aggregation of its own — so it stays a thin, testable value -/// the intents delegate to. +/// reports and published widget snapshot — no aggregation of its own — so it +/// stays a thin, testable value the intents delegate to. struct WhereIntentReader { let services: WhereServices var calendar = Calendar.whereIntents @@ -44,11 +43,4 @@ struct WhereIntentReader { } return try await regions(on: today) } - - /// The on-device recent-activity narrative for `window`. Rethrows the - /// summarizer's typed `ActivitySummaryUnavailableError` so the intent can - /// speak an actionable reason rather than a generic failure. - func recentActivity(_ window: RecentActivityWindow) async throws -> RecentActivitySummary { - try await services.recentActivity.summary(for: window) - } } diff --git a/Where/WhereIntents/Tests/ActivityWindowAppEnumTests.swift b/Where/WhereIntents/Tests/ActivityWindowAppEnumTests.swift deleted file mode 100644 index ff1ac9d2f..000000000 --- a/Where/WhereIntents/Tests/ActivityWindowAppEnumTests.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Testing -import WhereCore -@testable import WhereIntents - -/// `ActivityWindowAppEnum` round-trips losslessly with `RecentActivityWindow`. -struct ActivityWindowAppEnumTests { - @Test func roundTripsEveryWindow() { - for window in RecentActivityWindow.allCases { - #expect(ActivityWindowAppEnum(window).window == window) - } - } - - @Test func coversEveryDomainCase() { - let mapped = Set(ActivityWindowAppEnum.allCases.map(\.window)) - #expect(mapped == Set(RecentActivityWindow.allCases)) - } -} diff --git a/Where/WhereIntents/Tests/IntentStringsTests.swift b/Where/WhereIntents/Tests/IntentStringsTests.swift index 49509fc0e..8ada840a6 100644 --- a/Where/WhereIntents/Tests/IntentStringsTests.swift +++ b/Where/WhereIntents/Tests/IntentStringsTests.swift @@ -4,9 +4,8 @@ import Testing import WhereCore @testable import WhereIntents -/// Pins the dialog copy's variant selection — the count-driven singular / -/// plural / none branches and the recent-activity states — which is where the -/// spoken results could silently go wrong. +/// Pins the dialog copy's count-driven singular / plural / none branches, +/// which is where the spoken results could silently go wrong. struct IntentStringsTests { @Test func daysInRegionSelectsNoneSingularAndPlural() { let none = IntentStrings.daysInRegion(region: .california, days: 0, year: 2026) @@ -24,28 +23,4 @@ struct IntentStringsTests { #expect(IntentStrings.loggedTrip(dayCount: 1, regions: [.newYork]).contains("1 day")) #expect(IntentStrings.loggedTrip(dayCount: 5, regions: [.newYork]).contains("5 days")) } - - @Test func recentActivityReturnsTheSummaryTextVerbatim() { - let text = "You spent the week in California." - #expect(IntentStrings.recentActivity(.summary(text), window: .week) == text) - } - - @Test func recentActivityEmptyIsNonEmptyAndWindowSpecific() { - let week = IntentStrings.recentActivity(.empty, window: .week) - let month = IntentStrings.recentActivity(.empty, window: .month) - #expect(!week.isEmpty) - #expect(week != month) - } - - @Test func unavailableReasonsProduceDistinctNonEmptyCopy() { - let reasons: [ActivitySummaryUnavailableReason] = [ - .deviceNotEligible, - .appleIntelligenceNotEnabled, - .modelNotReady, - .unknown, - ] - let messages = reasons.map(IntentStrings.recentActivityUnavailable) - #expect(messages.allSatisfy { !$0.isEmpty }) - #expect(Set(messages).count == reasons.count) - } } diff --git a/Where/WhereIntents/Tests/WhereIntentsLogTests.swift b/Where/WhereIntents/Tests/WhereIntentsLogTests.swift index b26cd29d3..77a6f7ba3 100644 --- a/Where/WhereIntents/Tests/WhereIntentsLogTests.swift +++ b/Where/WhereIntents/Tests/WhereIntentsLogTests.swift @@ -22,10 +22,6 @@ struct WhereIntentsLogTests { #expect( String(describing: WhereIntentsLog.SpanName.perform(.logDay)) == "perform(log-day)", ) - #expect( - String(describing: WhereIntentsLog.SpanName.perform(.recentActivitySummary)) - == "perform(recent-activity-summary)", - ) #expect(String(describing: WhereIntentsLog.SpanName.awaitServices) == "awaitServices") } @@ -45,12 +41,10 @@ struct WhereIntentsLogTests { #expect(WhereIntentsLog.IntentName.allCases.allSatisfy { $0.budget > .zero }) } - @Test func onlyTheSlowByNatureIntentsGetSlack() { - // The summary waits on an on-device model that may still be warming and a - // trip backfills a whole range; a single aggregated read has no such + @Test func onlyTheSlowByNatureIntentGetsSlack() { + // A trip backfills a whole range; a single aggregated read has no such // excuse, so it stays on the tight default. let read = WhereIntentsLog.IntentName.todayRegions.budget - #expect(WhereIntentsLog.IntentName.recentActivitySummary.budget > read) #expect(WhereIntentsLog.IntentName.logTrip.budget > read) #expect(WhereIntentsLog.IntentName.logDay.budget == read) } diff --git a/Where/WhereUI/SnapshotTests/RecentActivitySummaryViewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/RecentActivitySummaryViewSnapshotTests.swift deleted file mode 100644 index 4e108ffd5..000000000 --- a/Where/WhereUI/SnapshotTests/RecentActivitySummaryViewSnapshotTests.swift +++ /dev/null @@ -1,10 +0,0 @@ -import SnapshotKitTesting -import Testing -@testable import WhereUI - -@MainActor -struct RecentActivitySummaryViewSnapshotTests { - @Test func recentActivity() async { - await assertSnapshots(of: RecentActivitySummaryView.self) - } -} diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPad.png index 2e0cb37bd..d12a6436a 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPad.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPad.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02ff314688b04e9751dbb6bde4bc0d14c244598c72fc52b91a0efee16e1475b0 -size 894163 +oid sha256:ee9faf1114851fc007176875d62a022a5223f47f8ee87e33b0f0ee0ef0098b8e +size 881829 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPad_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPad_accessibility.png index 79cb2f18a..6be95a212 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPad_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPad_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98fd56d5b2caa8fd53635ee7aa341a185a5e56f7fd13aa7e4f4b157be78b49d6 -size 1177582 +oid sha256:a0d39dae93bd8cd97839f9cfc6c367d715e5f91e731dbfb16ebaea3bd6a84d0b +size 1100182 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPad_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPad_ax5.png index 4a046c1c0..e3360480a 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPad_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPad_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1726437aeb00bcf8db97d80157ed86d0304dddfe3354d9e6ccd554afcd90d6fd -size 1606189 +oid sha256:b8aee22232b63bb35fe20b29d53aad495aac401db7c43098f4c03e5854876f5d +size 1224295 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPad_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPad_contrast.png index aed28fd3c..3a9f4ece1 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPad_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPad_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae12743799d9c6f5955bc3c4049cabfc9571453d54536d9a17053c74986e19c1 -size 986420 +oid sha256:cd2877ef6aa5b9b3aa30d80b86f94c0dbe29700d0872c49bfb8eda6af8ae2f4b +size 979933 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPad_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPad_dark.png index 9d93900ce..cd805541c 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPad_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPad_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9bf6dec728655767fce58cdb4360fb981554e59123ffc2757d4bffc969388f70 -size 889678 +oid sha256:a31379c824519dcc543a3253e2c956be7d1159293655517dca83202c5c4ca681 +size 882162 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPhone.png index a786a7b8a..f44df7f4a 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17f3ab5834354583102f913911c0e7b8416c4ee9dfe6dbbe416da840f335d77e -size 476850 +oid sha256:b41d7f2ff243341d90f700683fcfb2cd8b6dc6aee5eca0b4fdd2e61f4dbf5158 +size 417868 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPhone_accessibility.png index 45f488fa0..2a15ef3cb 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8750a6aa9e814e6df2284aa2b32a4860448c8d6ba4545e5f40a3879915830f6f -size 719748 +oid sha256:64f223caf91bdfb28ae941b301f59319794c5ab4fec28e91e1416b2189dfbdf4 +size 609447 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPhone_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPhone_ax5.png index 02f362547..ef86d048b 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPhone_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPhone_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8699cc02188f3d31865c5eef2176e353f6d42f16022ee2a2b4ce8ef1ca6bba48 -size 1206459 +oid sha256:0eb3b15485b5054cb8fe71efd3ad0c57f3f92929c97b9bbf83fc571576531abf +size 969919 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPhone_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPhone_contrast.png index 264f9d08b..08345f085 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPhone_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPhone_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04d482c56aa2427a0b4678d5a37ed9337a275f1a11a9e53b120ec761218d6a09 -size 505387 +oid sha256:d795f78674e032a2e1f2bb404643163b476cda9833d19e5c54a276df434ee444 +size 455396 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPhone_dark.png index ece144af1..2950bad34 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/InsightsAccuracyFeaturesViewSnapshotTests/insightsAccuracyFeatures.Default_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a438fd965adf3f6b413113fa2457c151f6e5bd4db56b4b697e55e8fe987af10 -size 469069 +oid sha256:ed3ab1a4fc43dbaf3561a79c9721c705b6f88bc11598f9bf62949f296c0ede38 +size 424823 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Empty_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Empty_iPhone.png deleted file mode 100644 index 8caaf61a1..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Empty_iPhone.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:68931227420c061f94b2fbdc80ccb4d1899f2869cd7217ccaf047f7a92d4232d -size 298614 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Empty_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Empty_iPhone_dark.png deleted file mode 100644 index b416fca10..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Empty_iPhone_dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ea8ff4e84e80e8025b4c5c451a92facd635becf8efd31cdd33bdc8738c048fd2 -size 293142 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Failed_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Failed_iPhone.png deleted file mode 100644 index 2f4bb731a..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Failed_iPhone.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a4574ef3740239eda4695e75256bca1586a4a554e148528b1b3ae0edd574f61b -size 292073 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Failed_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Failed_iPhone_dark.png deleted file mode 100644 index 3e0d201dd..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Failed_iPhone_dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ae55ff792a2a6a6e46996ee32c236c66c8698ae9a34c2d408cf5a5efc05dea3b -size 286074 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPad.png deleted file mode 100644 index 4c68e9e83..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPad.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d2c401cbe8446b11b2234851949ac778651bcc6d018ae3e5544643508bb40b4c -size 437195 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPad_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPad_accessibility.png deleted file mode 100644 index 36c55f68e..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPad_accessibility.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ec0e8e9c3b8d43d7f1fc91deccc479552bcb96b8ec62c709100f32a5b102e759 -size 554395 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPad_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPad_ax5.png deleted file mode 100644 index 571a21bb8..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPad_ax5.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e3c41d5d60c1a1405c336254e4285ec96f98232ca4d145d24221e00002e36573 -size 534819 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPad_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPad_contrast.png deleted file mode 100644 index 3584d95ad..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPad_contrast.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:569624764f079e6f9cbdd22fbb53737a14d0e50554089e49a170d518e6ae1d7e -size 429426 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPad_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPad_dark.png deleted file mode 100644 index 43bb58d82..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPad_dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6bb86844e33be08e699d7514d6a952c6384e02aa188dff1252ba191ffb527a8c -size 434513 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPhone.png deleted file mode 100644 index cb62468c8..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPhone.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:22ce28252fe8cadd9330d7ad31ceb85725518417d58efe4c1d6e93bd103c2b76 -size 296707 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPhone_accessibility.png deleted file mode 100644 index 12dc558fa..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPhone_accessibility.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:937b40995c41474a24f35a3446ee1a242cbc48ba55193e6fd4fc77d9b89a8533 -size 367660 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPhone_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPhone_ax5.png deleted file mode 100644 index cd3fc835e..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPhone_ax5.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:da731f73b61d8bedf78cc7111c560c93984aff5ba50b463fc867098df71abea0 -size 403892 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPhone_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPhone_contrast.png deleted file mode 100644 index f0587367b..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPhone_contrast.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a8b5bd90f327967f604441152a8048eeb9b092c28a7e63413b0b0c1107012662 -size 290926 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPhone_dark.png deleted file mode 100644 index 3c90c7ecb..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Loaded_iPhone_dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6b407afced44954fd1193db76f39498ba4c09a2763fe2d8fc3a293c6418a5f21 -size 290780 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Unavailable_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Unavailable_iPhone.png deleted file mode 100644 index fe63df583..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Unavailable_iPhone.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1c589d6920e6c39fa8865914efdd57e11e8516377b57304caf0c719a5e81ce21 -size 297275 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Unavailable_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Unavailable_iPhone_dark.png deleted file mode 100644 index 9c07fc183..000000000 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RecentActivitySummaryViewSnapshotTests/recentActivity.Unavailable_iPhone_dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0e5cc743a078c8b08f1c0e7a91fde0a29bf0412d1ec867465faf818a695b4809 -size 291448 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPad.png index d43b26213..d6c543857 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPad.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPad.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3eae943599b176b9bee4f8933725baa68e39be2a165abc89c2329facda546c92 -size 1227580 +oid sha256:ebacfb9bcb1f924c6d8947edae38dc399850927683d9beda24377b7263fc49ad +size 1125463 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPad_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPad_accessibility.png index ef1a47508..a6c0b70b0 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPad_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPad_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8e26af1408632fab5a851a4864ac4471ad5f349f5ab952dcef1925ec2594663 -size 1732364 +oid sha256:0fd727bbcbe3c5927c3622bbb2af2a688ae1c343ae216e608bc5916fc4e3c41b +size 1585985 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPad_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPad_ax5.png index 39886089d..90aaaee3e 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPad_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPad_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ddea94e47af36c7f2642c2241c7b93233c288f2d15115c48ff40efb3b48dfd2 -size 2317394 +oid sha256:efd63ab48563ba0eadee0c14fcc8a215945b3f07d4d46ffc420ba21abe0eb9bc +size 2097882 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPad_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPad_contrast.png index dfdaf222f..e00e46d9a 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPad_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPad_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a047042edcb24fdd0bacbdfc69b5c8818015a5f731b33697e72d06889c50da07 -size 1319402 +oid sha256:364bf607513148a57821ce21393ee489101e7ed956fbfadb75eb3a0955593bc5 +size 1232693 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPad_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPad_dark.png index b34f786c4..95c993058 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPad_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPad_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:80ef6f12cbf4e1caf5761bef6141c977cf0420b04a2a3aa79b950315e0f1cbbd -size 1209646 +oid sha256:fc475c709f314b76fb250c0671b01a16ab37f41d5eaa31a3df895b0575e2d0be +size 1110603 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPhone.png index 72e575b4c..e046be5cb 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b172c44778960a2deb84c1c6155056edfd7b1299dcd645de9bf7c6b9cfa4c00 -size 873548 +oid sha256:eacd98171db4f02d43569c5c9d3365ed5a6cd4c4290b7054885c8b2d280d0401 +size 816810 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPhone_accessibility.png index 2bd1cabe8..38d3e8213 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e79792bd7f23a0768d4693222304c79154dfdd32973fb43ae5e0984f854e1f5b -size 1336676 +oid sha256:161b4bbf6cebc140047e0f9920eba08f68f3a021bbe64b216bd7e921ae90276a +size 1227667 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPhone_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPhone_ax5.png index 06e53ac31..06edbda8a 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPhone_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPhone_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35a9361f8670be7e1e3c8d09aa420b12d619dfdc40a4550a52876c1780d17081 -size 2121914 +oid sha256:7f66618628513cfc20e780ffc19bcb79deb82378265c6e4412412190905dfa12 +size 1888850 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPhone_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPhone_contrast.png index 649d90336..c1d90c252 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPhone_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPhone_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c5811314b9495a17164bd9b26379c25b6c322e258b6b19cd5c4798df494fbaa -size 921659 +oid sha256:82a17167c0559c7f47407a6a2b0820c0dd4fd8bd6f2edf22d1b62969aab6a3aa +size 872233 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPhone_dark.png index 5b83a9065..f4d477e7a 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/SiriFeaturesViewSnapshotTests/siriFeatures.Default_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7026139d9d25bc533173fb6b065dec8d34a1c64d81cb07b6f8ae20c33304032 -size 849092 +oid sha256:6917a63f48a94c616fb1833d042876dd669a5f81abe770e8787d31b064d72788 +size 790846 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Empty_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Empty_iPhone.png index af631ac15..0ec8be766 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Empty_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Empty_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dad9e7c911fddc398d83692c82ef7e5146ac7c29638889c195ccf0b79ba69606 -size 742511 +oid sha256:57cc47bf245922b7fc0987c62753bed223ffaf69aeaeffa00ea6b0a8c6eefde9 +size 730673 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Empty_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Empty_iPhone_dark.png index e006ea316..e76e53a84 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Empty_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Empty_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a22f9f5db2bd9e5d4bebcb63bdd094021591b9fc4a5ffbdb0fa5f8d3ea4c3a20 -size 701940 +oid sha256:d205a481b9db0be6c9bd0b0dbc382b1058d64ca12c5fc8b4f0673436045bd13d +size 687963 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad.png index d0b035166..1fe36ac98 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63e0721c2d1b276ce8bcad93ef4fe9da0421832abf0337d9c5f318a116a576d7 -size 1129475 +oid sha256:b2fe57a92dac3ac195fcd337c4326ea90f99180beb0b64ff65b7a60a278a422e +size 1116250 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_accessibility.png index dae390b0f..80323e166 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:556fe0890da7c20e1d109b6573db89246f827517f16875bca7bc368390fc7b67 -size 3565301 +oid sha256:03789c1527a0fb752b7a685dc3791fad8d30fd2bd658980abfa1138ecdeacd43 +size 3546624 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_ax5.png index 801310c22..cde098a04 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c90505f1aeef80edbceffe6b586d427aaf82720c5ebf2b0abaa385cafe674524 -size 1835845 +oid sha256:0a54dbaea3d0760db143e4b88c25661b8d88947a03414421dc14570be0083331 +size 1820781 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_contrast.png index 4076df370..fa27910d4 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72fb188be14093ed10092663646bd743a420319438a9c00bd2eb92abf0fcf1b6 -size 1125887 +oid sha256:a8a9decabeaf6f702179a5809b1992627bb1f360c401bda33bb2ab073ff3f244 +size 1114251 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_dark.png index 93a33f207..a2c56451f 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:837ec3476025546c3fb48e87be475bc9c73405314f5ab17854578a0a1cfb6124 -size 1102171 +oid sha256:a200aef4b8398f14467fb36dad9ad77576e69804bf490ba20ca1250e917b4f42 +size 1088308 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone.png index e37c31bf9..9d67d0816 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abb6f2d537e704aa6cab4086e0cf216d7c39079cf9e79b572fe34da21cbe00a3 -size 839562 +oid sha256:a6bae029b0c81cb5fdfe4264f452cea07d2a21fbd87bdd554f67a85d7cdffb44 +size 827526 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_accessibility.png index bc6ef4505..81199c16d 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fee23b51b281d1e8d95a4b487222a3f43105c7f889b6bca47850c4e24dfda0ea -size 3183826 +oid sha256:4042b72fe5964199488a32cbfb0c67001be159ec9be72a3828bd173f047106f3 +size 3168246 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_ax5.png index e2642a1d2..f630a36d9 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:411100ea8116452e2f7ade54276a33e546c833eb75cd53ee9dd0e9b63a046103 -size 1059305 +oid sha256:2c592007a6c9289324e59cd7ed29637cbb9639ab2ad683633cfb1f5ddaff4555 +size 1047009 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_contrast.png index c68326b50..70b68f6e1 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9f5e97b867e37d70cbf72caa3d9b702fe477574e9a54005a75770ad32a07698 -size 839678 +oid sha256:dd630943480f8cb32a863d657bb2ec7463d05f8ffe8519070ba983500008e401 +size 829047 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_dark.png index e37d9c81f..d0884ab69 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a8b50351745bdf29a0ce2c56eefb82b22b106d96c61d4dd5ad7174d1ba1447f -size 818861 +oid sha256:70f1e6366dc1ee23a06baa7961ed01cfc2ddb28ecf0677cc0f2934aedf3584d5 +size 805380 diff --git a/Where/WhereUI/Sources/Developer/Flyover/WhereFlyoverCatalog.swift b/Where/WhereUI/Sources/Developer/Flyover/WhereFlyoverCatalog.swift index d1dac7a87..27efe1583 100644 --- a/Where/WhereUI/Sources/Developer/Flyover/WhereFlyoverCatalog.swift +++ b/Where/WhereUI/Sources/Developer/Flyover/WhereFlyoverCatalog.swift @@ -77,7 +77,6 @@ YearView.flyoverData, CalendarContentView.yearFlyoverData, PresenceTimelineList.flyoverData, - RecentActivitySummaryView.flyoverData, ] } diff --git a/Where/WhereUI/Sources/Logging/RecentActivityModelLog.swift b/Where/WhereUI/Sources/Logging/RecentActivityModelLog.swift deleted file mode 100644 index f7926ca80..000000000 --- a/Where/WhereUI/Sources/Logging/RecentActivityModelLog.swift +++ /dev/null @@ -1,24 +0,0 @@ -import PeriscopeCore - -/// Structured events for `RecentActivityModel`. Both an unavailable on-device -/// model and a generation failure leave an honest UI error, so they log at -/// `.warning`. -enum RecentActivityModelLog: LogEvent { - case summaryUnavailable(reason: String) - case summaryFailed(description: String) - - static let eventName = "RecentActivityModel" - - var level: LogLevel { - .warning - } - - var message: String { - switch self { - case let .summaryUnavailable(reason): - "Recent-activity summary unavailable: \(reason)" - case let .summaryFailed(description): - "Recent-activity summary failed: \(description)" - } - } -} diff --git a/Where/WhereUI/Sources/Model/RecentActivityModel.swift b/Where/WhereUI/Sources/Model/RecentActivityModel.swift deleted file mode 100644 index b063fa228..000000000 --- a/Where/WhereUI/Sources/Model/RecentActivityModel.swift +++ /dev/null @@ -1,67 +0,0 @@ -import Foundation -import Observation -import PeriscopeCore -import WhereCore - -/// View-scoped model for the "last 24 hours" on-device summary. Mirrors the -/// `RecentActivitySummarizer` output into a `LoadState` the sheet renders, -/// distinguishing a real summary from an empty window, an unavailable model, -/// and an outright failure so the UI can respond to each honestly. -@MainActor -@Observable -public final class RecentActivityModel { - /// Where the summary is in its load lifecycle. - public enum LoadState: Equatable { - case idle - case loading - case loaded(String) - /// The window held no tracked locations — distinct from a blank summary. - case empty - /// The on-device model can't run; carries the reason so the UI can guide - /// the user (e.g. enable Apple Intelligence). - case unavailable(ActivitySummaryUnavailableReason) - /// Generation failed; carries a user-presentable message. - case failed(String) - } - - public private(set) var loadState: LoadState = .idle - - /// The look-back window the summary covers. The sheet's segmented control - /// binds to this directly; changing it triggers a fresh `load()`. - public var window: RecentActivityWindow = .day - - private let services: WhereServices - private static let logger = WhereLog.recentActivity(RecentActivityModelLog.self) - - init(services: WhereServices) { - self.services = services - } - - /// Generate (or regenerate) the summary for the selected `window`. Maps an - /// unavailable model and a generation failure to distinct states and logs - /// both — never a silent empty result that reads like success. - public func load() async { - loadState = .loading - do { - switch try await services.recentActivity.summary(for: window) { - case let .summary(text): - loadState = .loaded(text) - case .empty: - loadState = .empty - } - } catch let error as ActivitySummaryUnavailableError { - loadState = .unavailable(error.reason) - Self.logger { .summaryUnavailable(reason: String(describing: error.reason)) } - } catch { - loadState = .failed(error.localizedDescription) - Self.logger { .summaryFailed(description: error.localizedDescription) } - } - } - - #if DEBUG - /// Force a state for previews/tests without running the generator. - func previewLoad(_ state: LoadState) { - loadState = state - } - #endif -} diff --git a/Where/WhereUI/Sources/Preview/PreviewSupport.swift b/Where/WhereUI/Sources/Preview/PreviewSupport.swift index a5e6a5cca..e0a822a61 100644 --- a/Where/WhereUI/Sources/Preview/PreviewSupport.swift +++ b/Where/WhereUI/Sources/Preview/PreviewSupport.swift @@ -488,19 +488,6 @@ return resolve } - // MARK: - Recent activity (24h summary sheet) - - /// A recent-activity model forced into a chosen state (no generator run), - /// so the summary sheet's states drop straight into a `#Preview`. - @MainActor - public static func recentActivityModel( - state: RecentActivityModel.LoadState, - ) -> RecentActivityModel { - let model = RecentActivityModel(services: previewServices()) - model.previewLoad(state) - return model - } - // MARK: - Logged days (manual entries sheet) /// A believable set of manual day entries across the sample year — a mix diff --git a/Where/WhereUI/Sources/Primary/RecentActivitySummaryView.swift b/Where/WhereUI/Sources/Primary/RecentActivitySummaryView.swift deleted file mode 100644 index fca87f17a..000000000 --- a/Where/WhereUI/Sources/Primary/RecentActivitySummaryView.swift +++ /dev/null @@ -1,187 +0,0 @@ -import PeriscopeCore -import SnapshotKit -import SwiftUI -import WhereCore - -/// A sheet showing an on-device, AI-generated summary of a selectable look-back -/// window of tracked locations (24 hours, a week, a month, or the year so far). -/// Presented from the Primary tab. A segmented control at the top picks the -/// window; each `RecentActivityModel.LoadState` renders distinctly — a real -/// summary (streamed in with a typewriter reveal), an empty window, an -/// unavailable model (with guidance), or a failure — and a refresh regenerates. -struct RecentActivitySummaryView: View { - @Environment(\.dismiss) private var dismiss - @Environment(\.stylesheet) private var stylesheet - - @State private var model: RecentActivityModel - - init(report: YearReportModel) { - _model = State(initialValue: RecentActivityModel(services: report.services)) - } - - #if DEBUG - /// Preview seam: inject a model already in a chosen state. - init(model: RecentActivityModel) { - _model = State(initialValue: model) - } - #endif - - var body: some View { - NavigationStack { - content - .safeAreaInset(edge: .top) { windowPicker } - .animation(.smooth, value: model.loadState) - .navigationTitle(WhereFormat.recentActivityTitle(model.window)) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button(String(localized: .commonDone)) { dismiss() } - } - ToolbarItem(placement: .topBarLeading) { - Button { - Task { await model.load() } - } label: { - Label( - String(localized: .recentActivityRefresh), - systemImage: "arrow.clockwise", - ) - } - .disabled(model.loadState == .loading) - } - } - .task { - if model.loadState == .idle { await model.load() } - } - // Regenerate for the newly picked window. The picker is disabled - // while loading (see `windowPicker`), so this can't fire a second - // load over an in-flight one. - .onChange(of: model.window) { - Task { await model.load() } - } - } - // Log View Mode: reveal an inspect badge for recent-activity summary - // events. A no-op in release. - .debugLogInspectable(WhereLog.recentActivity(RecentActivityModelLog.self)) - } - - /// Segmented control for the summary window, pinned under the navigation - /// bar. Bound straight to the observable `window`; the `.onChange` above - /// turns a change into a reload. Disabled while a summary is generating so - /// selections can't race an in-flight load. - private var windowPicker: some View { - Picker(String(localized: .recentActivityWindowPickerLabel), selection: $model.window) { - ForEach(RecentActivityWindow.allCases, id: \.self) { window in - Text(WhereFormat.recentActivityWindowLabel(window)).tag(window) - } - } - .pickerStyle(.segmented) - .disabled(model.loadState == .loading) - .padding(.horizontal) - .padding(.vertical, stylesheet.spacing.medium) - .background(.bar) - } - - /// Each state fades into the next (see `.animation` in `body`) rather than - /// hard-cutting — a crossfade suits swapping between a spinner, prose, and a - /// `ContentUnavailableView`. - @ViewBuilder - private var content: some View { - switch model.loadState { - case .idle, .loading: - AppIconLoadingView(caption: String(localized: .recentActivityLoading)) - .transition(.opacity) - case let .loaded(text): - summary(text) - .transition(.opacity) - case .empty: - ContentUnavailableView { - Label( - String(localized: .recentActivityEmptyTitle), - systemImage: "location.slash", - ) - } description: { - Text(WhereFormat.recentActivityEmptyDescription(model.window)) - } - .transition(.opacity) - case let .unavailable(reason): - ContentUnavailableView { - Label( - String(localized: .recentActivityUnavailableTitle), - systemImage: "sparkles.slash", - ) - } description: { - Text(WhereFormat.recentActivityUnavailableMessage(reason)) - } - .transition(.opacity) - case let .failed(message): - ContentUnavailableView { - Label( - String(localized: .recentActivityFailedTitle), - systemImage: "exclamationmark.triangle", - ) - } description: { - Text(message) - } - .transition(.opacity) - } - } - - private func summary(_ text: String) -> some View { - ScrollView { - VStack(alignment: .leading, spacing: stylesheet.spacing.medium) { - TypewriterText(text: text) - .font(.body) - .frame(maxWidth: .infinity, alignment: .leading) - Text(WhereFormat.recentActivityFooter(model.window)) - .font(.footnote) - .foregroundStyle(.secondary) - } - .padding() - } - } -} - -#if DEBUG - extension RecentActivitySummaryView: SnapshotProviding { - /// Capture the full settled scroll content; fixed viewport coverage is - /// reserved for non-scrolling subjects. - static var snapshots: [SnapshotCase] { - whereSnapshot(name: "Loaded", configurations: .fullContentScreenDefaults) { - RecentActivitySummaryView( - model: PreviewSupport.recentActivityModel( - state: .loaded("You were in California, then New York."), - ), - ) - } - whereSnapshot(name: "Empty", configurations: .phoneLightDark) { - RecentActivitySummaryView(model: PreviewSupport.recentActivityModel(state: .empty)) - } - whereSnapshot(name: "Unavailable", configurations: .phoneLightDark) { - RecentActivitySummaryView( - model: PreviewSupport.recentActivityModel( - state: .unavailable(.appleIntelligenceNotEnabled), - ), - ) - } - whereSnapshot(name: "Failed", configurations: .phoneLightDark) { - RecentActivitySummaryView( - model: PreviewSupport - .recentActivityModel(state: .failed("Something went wrong.")), - ) - } - } - } - - #Preview { - RecentActivitySummaryView.snapshotPreviews - } -#endif - -#if DEBUG - extension RecentActivitySummaryView: WhereFlyoverProviding { - static let flyoverData = WhereFlyoverData.snapshots( - RecentActivitySummaryView.self, - title: "Recent Activity", - ) - } -#endif diff --git a/Where/WhereUI/Sources/Resources/Localizable.xcstrings b/Where/WhereUI/Sources/Resources/Localizable.xcstrings index 0e303dabb..57431b1e2 100644 --- a/Where/WhereUI/Sources/Resources/Localizable.xcstrings +++ b/Where/WhereUI/Sources/Resources/Localizable.xcstrings @@ -3911,17 +3911,6 @@ } } }, - "primary.recentActivity" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Recent activity" - } - } - } - }, "primary.timeline" : { "extractionState" : "manual", "localizations" : { @@ -3933,292 +3922,6 @@ } } }, - "recentActivity.empty.description.day" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No locations were recorded in the last 24 hours." - } - } - } - }, - "recentActivity.empty.description.month" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No locations were recorded in the past month." - } - } - } - }, - "recentActivity.empty.description.week" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No locations were recorded in the past week." - } - } - } - }, - "recentActivity.empty.description.yearToDate" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No locations were recorded so far this year." - } - } - } - }, - "recentActivity.empty.title" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nothing tracked" - } - } - } - }, - "recentActivity.failed.title" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Couldn't summarize" - } - } - } - }, - "recentActivity.footer.day" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "An on-device summary of where you've been in the last 24 hours. Your location never leaves your device." - } - } - } - }, - "recentActivity.footer.month" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "An on-device summary of where you've been over the past month. Your location never leaves your device." - } - } - } - }, - "recentActivity.footer.week" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "An on-device summary of where you've been over the past week. Your location never leaves your device." - } - } - } - }, - "recentActivity.footer.yearToDate" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "An on-device summary of where you've been so far this year. Your location never leaves your device." - } - } - } - }, - "recentActivity.loading" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Summarizing…" - } - } - } - }, - "recentActivity.refresh" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Refresh" - } - } - } - }, - "recentActivity.title.day" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Last 24 hours" - } - } - } - }, - "recentActivity.title.month" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Past month" - } - } - } - }, - "recentActivity.title.week" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Past week" - } - } - } - }, - "recentActivity.title.yearToDate" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Year so far" - } - } - } - }, - "recentActivity.unavailable.appleIntelligenceNotEnabled" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Turn on Apple Intelligence in Settings to generate summaries." - } - } - } - }, - "recentActivity.unavailable.deviceNotEligible" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "This device doesn't support on-device summaries." - } - } - } - }, - "recentActivity.unavailable.modelNotReady" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The on-device model is still getting ready. Try again shortly." - } - } - } - }, - "recentActivity.unavailable.title" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Summaries unavailable" - } - } - } - }, - "recentActivity.unavailable.unknown" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "On-device summaries aren't available right now." - } - } - } - }, - "recentActivity.window.day" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "24 Hours" - } - } - } - }, - "recentActivity.window.month" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Month" - } - } - } - }, - "recentActivity.window.pickerLabel" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Summary range" - } - } - } - }, - "recentActivity.window.week" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Week" - } - } - } - }, - "recentActivity.window.yearToDate" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Year" - } - } - } - }, "regionCustomize.color" : { "extractionState" : "manual", "localizations" : { @@ -6286,40 +5989,6 @@ } } }, - "settings.explore.insights.activity.generic" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Turn your recent location history into a concise narrative of where you’ve been." - } - } - } - }, - "settings.explore.insights.activity.personalized" : { - "comment" : "Activity preview using real data. Arguments are the logged-day count and localized region list.", - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Your latest %1$lld logged days include %2$@." - } - } - } - }, - "settings.explore.insights.activity.title" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Recent Activity Summary" - } - } - } - }, "settings.explore.insights.allClear" : { "extractionState" : "manual", "localizations" : { @@ -6359,7 +6028,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Recent Activity runs only when you ask. Where surfaces Resolve when it finds a gap, drift, abrupt change, or flight-day anomaly." + "value" : "Where surfaces Resolve when it finds a gap, drift, abrupt change, or flight-day anomaly." } } } @@ -6387,28 +6056,6 @@ } } }, - "settings.explore.insights.onDevice" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Generated privately on this device" - } - } - } - }, - "settings.explore.insights.openActivity" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Open Recent Activity" - } - } - } - }, "settings.explore.insights.openResolve" : { "extractionState" : "manual", "localizations" : { @@ -6437,7 +6084,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "See the story in your travels—and catch gaps before they become questions." + "value" : "Catch gaps, drift, and other issues before they affect your records." } } } @@ -6807,17 +6454,6 @@ } } }, - "settings.explore.siri.personalized.recent.response" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Your past week includes %@." - } - } - } - }, "settings.explore.siri.personalized.today.response.empty" : { "extractionState" : "manual", "localizations" : { @@ -6840,39 +6476,6 @@ } } }, - "settings.explore.siri.recent.request" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Summarize where I’ve been this week." - } - } - } - }, - "settings.explore.siri.recent.response" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "You spent most of the week in California, with a trip to New York." - } - } - } - }, - "settings.explore.siri.recent.title" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Recent Activity" - } - } - } - }, "settings.explore.siri.row" : { "extractionState" : "manual", "localizations" : { @@ -7368,7 +6971,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "AI, Apple Intelligence, summary, recent activity, issue, resolve, missing day, drift, border, flight, accuracy" + "value" : "issue, resolve, missing day, drift, border, flight, accuracy" } } } diff --git a/Where/WhereUI/Sources/Settings/AppIconOption.swift b/Where/WhereUI/Sources/Settings/AppIconOption.swift index ccd37fd8e..d0ceb36c5 100644 --- a/Where/WhereUI/Sources/Settings/AppIconOption.swift +++ b/Where/WhereUI/Sources/Settings/AppIconOption.swift @@ -107,8 +107,8 @@ enum AppIconCatalog { /// The preview-catalog image name of the currently selected icon, resolved /// from the live `UIApplication.shared.alternateIconName` against the /// manifest and falling back to the bundled "Classic" art. Shared by every - /// in-app surface that renders the selected icon (launch splash, the - /// recent-activity loading indicator) so they stay in lockstep. + /// in-app surface that renders the selected icon (launch splash and shared + /// loading states) so they stay in lockstep. @MainActor static func liveSelectedPreviewImageName() -> String { let options = loadedOptions() let selected = selectedOption( diff --git a/Where/WhereUI/Sources/Settings/FeaturePreviews/InsightsAccuracy/FeatureRecentActivityPreview.swift b/Where/WhereUI/Sources/Settings/FeaturePreviews/InsightsAccuracy/FeatureRecentActivityPreview.swift deleted file mode 100644 index 3356f692c..000000000 --- a/Where/WhereUI/Sources/Settings/FeaturePreviews/InsightsAccuracy/FeatureRecentActivityPreview.swift +++ /dev/null @@ -1,44 +0,0 @@ -import SwiftUI - -/// A deterministic preview of the private on-device activity narrative. The -/// real Foundation Models generation starts only after the user opens it. -struct FeatureRecentActivityPreview: View { - let example: FeatureDiscoveryPresentation.ActivityExample - - @Environment(\.stylesheet) private var stylesheet - - var body: some View { - let panelStyle = stylesheet.featureDiscovery.marketingPanel - FeatureMarketingPanel { - VStack(alignment: .leading, spacing: panelStyle.contentSpacing) { - Label( - String(localized: .settingsExploreInsightsActivityTitle), - systemImage: "sparkles", - ) - .font(.headline) - - Text(example.summary) - .font(.body) - .fixedSize(horizontal: false, vertical: true) - - Label( - String(localized: .settingsExploreInsightsOnDevice), - systemImage: "iphone.gen3.radiowaves.left.and.right", - ) - .font(.footnote) - .foregroundStyle(.secondary) - } - } - .accessibilityElement(children: .combine) - } -} - -#if DEBUG - #Preview { - FeatureRecentActivityPreview( - example: .init(summary: "Your latest 14 logged days include California and New York."), - ) - .padding() - .whereBroadwayRoot() - } -#endif diff --git a/Where/WhereUI/Sources/Settings/FeaturePreviews/InsightsAccuracy/InsightsAccuracyFeaturesView.swift b/Where/WhereUI/Sources/Settings/FeaturePreviews/InsightsAccuracy/InsightsAccuracyFeaturesView.swift index 792172489..d0481e97b 100644 --- a/Where/WhereUI/Sources/Settings/FeaturePreviews/InsightsAccuracy/InsightsAccuracyFeaturesView.swift +++ b/Where/WhereUI/Sources/Settings/FeaturePreviews/InsightsAccuracy/InsightsAccuracyFeaturesView.swift @@ -1,14 +1,13 @@ import SnapshotKit import SwiftUI -/// Markets Where's on-device travel narrative and its automatic data-quality -/// detection, while deferring generation and fixes until an explicit action. +/// Markets Where's automatic data-quality detection while deferring fixes +/// until an explicit action. struct InsightsAccuracyFeaturesView: View { let report: YearReportModel let focus: SettingsFocus? - let presentation: FeatureDiscoveryPresentation - @State private var presentedSheet: Sheet? + @State private var showingResolution = false @Environment(\.stylesheet) private var stylesheet var body: some View { @@ -26,26 +25,9 @@ struct InsightsAccuracyFeaturesView: View { .listRowSeparator(.hidden) .staggeredReveal(order: 0) - Section { - FeatureRecentActivityPreview(example: presentation.activityExample) - .featureMarketingRow(order: 1) - .settingsRow(Item.recentActivity, restingBackground: .clear) - - FeatureMarketingPanel { - Button(action: showRecentActivity) { - actionLabel( - String(localized: .settingsExploreInsightsOpenActivity), - systemImage: "sparkles", - ) - } - .frame(maxWidth: .infinity, alignment: .leading) - } - .featureMarketingRow(order: 2) - } - Section { FeatureDataAccuracyPreview(issueCount: report.dataIssueCount) - .featureMarketingRow(order: 3) + .featureMarketingRow(order: 1) .settingsRow(Item.dataAccuracy, restingBackground: .clear) if report.dataIssueCount > 0 { @@ -58,14 +40,14 @@ struct InsightsAccuracyFeaturesView: View { } .frame(maxWidth: .infinity, alignment: .leading) } - .featureMarketingRow(order: 4) + .featureMarketingRow(order: 2) } } footer: { VStack(alignment: .leading, spacing: stylesheet.spacing.medium) { Text(String(localized: .settingsExploreInsightsFooter)) FeatureDiscoveryDataFooter() } - .staggeredReveal(order: 5) + .staggeredReveal(order: 3) } } .scrollContentBackground(.hidden) @@ -74,11 +56,8 @@ struct InsightsAccuracyFeaturesView: View { } .navigationTitle("") .navigationBarTitleDisplayMode(.inline) - .sheet(item: $presentedSheet) { sheet in - switch sheet { - case .recentActivity: RecentActivitySummaryView(report: report) - case .resolution: ResolutionView(report: report) - } + .sheet(isPresented: $showingResolution) { + ResolutionView(report: report) } } @@ -92,21 +71,8 @@ struct InsightsAccuracyFeaturesView: View { } } - private func showRecentActivity() { - presentedSheet = .recentActivity - } - private func showResolution() { - presentedSheet = .resolution - } - - private enum Sheet: Hashable, Identifiable { - case recentActivity - case resolution - - var id: Self { - self - } + showingResolution = true } } @@ -116,14 +82,10 @@ extension InsightsAccuracyFeaturesView: SettingsSection { } enum Item: SettingsItem { - case recentActivity case dataAccuracy var title: String { - switch self { - case .recentActivity: String(localized: .settingsExploreInsightsActivityTitle) - case .dataAccuracy: String(localized: .settingsExploreInsightsAccuracyTitle) - } + String(localized: .settingsExploreInsightsAccuracyTitle) } var keywords: [String] { @@ -143,7 +105,6 @@ extension InsightsAccuracyFeaturesView: SettingsSection { InsightsAccuracyFeaturesView( report: reportWithIssues(), focus: nil, - presentation: PreviewSupport.featureDiscoveryPresentation(), ) } } @@ -166,10 +127,7 @@ extension InsightsAccuracyFeaturesView: SettingsSection { static let flyoverData = WhereFlyoverData.snapshots( InsightsAccuracyFeaturesView.self, title: "Insights & Accuracy", - routes: [ - .modal(to: RecentActivitySummaryView.flyoverID), - .modal(to: ResolutionView.flyoverID), - ], + routes: [.modal(to: ResolutionView.flyoverID)], ) } #endif diff --git a/Where/WhereUI/Sources/Settings/FeaturePreviews/Shared/FeatureDiscoveryPresentation.swift b/Where/WhereUI/Sources/Settings/FeaturePreviews/Shared/FeatureDiscoveryPresentation.swift index 678fd00fd..ded76b9be 100644 --- a/Where/WhereUI/Sources/Settings/FeaturePreviews/Shared/FeatureDiscoveryPresentation.swift +++ b/Where/WhereUI/Sources/Settings/FeaturePreviews/Shared/FeatureDiscoveryPresentation.swift @@ -16,19 +16,14 @@ struct FeatureDiscoveryPresentation { let resultSubtitle: String } - struct ActivityExample: Equatable { - let summary: String - } - - /// Two weeks of recorded days is enough to make counts, recent activity, - /// and widget totals representative rather than incidental. + /// Two weeks of recorded days is enough to make counts and widget totals + /// representative rather than incidental. static let minimumLoggedDayCount = 14 let usesUserData: Bool let widgetSnapshot: WidgetSnapshot let lockScreenDate: Date let spotlightExample: SpotlightExample - let activityExample: ActivityExample private let siriExamples: [SiriIntentFeature: SiriExample] @@ -73,9 +68,6 @@ struct FeatureDiscoveryPresentation { )), resultSubtitle: String(localized: .settingsExploreSpotlightResultGeneric), ) - activityExample = ActivityExample( - summary: String(localized: .settingsExploreInsightsActivityGeneric), - ) siriExamples = [:] return } @@ -97,32 +89,14 @@ struct FeatureDiscoveryPresentation { calendar: calendar, ) spotlightExample = Self.spotlightExample(report: report) - activityExample = Self.activityExample(relevantDays: relevantDays) siriExamples = Self.siriExamples( report: report, relevantDays: relevantDays, referenceDay: referenceDay, - referenceDate: referenceDate, calendar: calendar, ) } - private static func activityExample(relevantDays: [DayPresence]) -> ActivityExample { - let recentDays = relevantDays.suffix(minimumLoggedDayCount) - let regions = Set(recentDays.flatMap(\.regions)) - guard !regions.isEmpty else { - return ActivityExample( - summary: String(localized: .settingsExploreInsightsActivityGeneric), - ) - } - return ActivityExample(summary: String( - localized: .settingsExploreInsightsActivityPersonalized( - minimumLoggedDayCount, - regionList(regions), - ), - )) - } - private static func spotlightExample(report: YearReport) -> SpotlightExample { guard let region = RegionRanking(report: report).primary.first else { return SpotlightExample( @@ -153,7 +127,6 @@ struct FeatureDiscoveryPresentation { report: YearReport, relevantDays: [DayPresence], referenceDay: CalendarDay, - referenceDate: Date, calendar: Calendar, ) -> [SiriIntentFeature: SiriExample] { var examples: [SiriIntentFeature: SiriExample] = [:] @@ -199,24 +172,6 @@ struct FeatureDiscoveryPresentation { ) } - let recentInterval = RecentActivityWindow.week.interval( - now: referenceDate, - calendar: calendar, - ) - let recentStartDay = CalendarDay(from: recentInterval.start, in: calendar) - let recentDays = relevantDays.filter { - $0.day >= recentStartDay && $0.day <= referenceDay - } - let recentRegions = Set(recentDays.flatMap(\.regions)) - if !recentRegions.isEmpty { - examples[.recentActivity] = SiriExample( - request: String(localized: .settingsExploreSiriRecentRequest), - response: String(localized: .settingsExploreSiriPersonalizedRecentResponse( - regionList(recentRegions), - )), - ) - } - return examples } diff --git a/Where/WhereUI/Sources/Settings/FeaturePreviews/Siri/SiriFeaturesView.swift b/Where/WhereUI/Sources/Settings/FeaturePreviews/Siri/SiriFeaturesView.swift index 7f6347ea2..2d7550bbc 100644 --- a/Where/WhereUI/Sources/Settings/FeaturePreviews/Siri/SiriFeaturesView.swift +++ b/Where/WhereUI/Sources/Settings/FeaturePreviews/Siri/SiriFeaturesView.swift @@ -85,7 +85,6 @@ extension SiriFeaturesView: SettingsSection { case todayRegions case daysInRegion case regionOnDate - case recentActivity case logDay case logTrip case spotlight @@ -95,7 +94,6 @@ extension SiriFeaturesView: SettingsSection { case .todayRegions: String(localized: .settingsExploreSiriTodayTitle) case .daysInRegion: String(localized: .settingsExploreSiriDaysTitle) case .regionOnDate: String(localized: .settingsExploreSiriDateTitle) - case .recentActivity: String(localized: .settingsExploreSiriRecentTitle) case .logDay: String(localized: .settingsExploreSiriLogDayTitle) case .logTrip: String(localized: .settingsExploreSiriLogTripTitle) case .spotlight: String(localized: .settingsExploreSpotlightTitle) diff --git a/Where/WhereUI/Sources/Settings/FeaturePreviews/Siri/SiriIntentFeature.swift b/Where/WhereUI/Sources/Settings/FeaturePreviews/Siri/SiriIntentFeature.swift index 1fd3290b4..685ffe018 100644 --- a/Where/WhereUI/Sources/Settings/FeaturePreviews/Siri/SiriIntentFeature.swift +++ b/Where/WhereUI/Sources/Settings/FeaturePreviews/Siri/SiriIntentFeature.swift @@ -7,7 +7,6 @@ enum SiriIntentFeature: CaseIterable, Hashable { case todayRegions case daysInRegion case regionOnDate - case recentActivity case logDay case logTrip @@ -16,7 +15,6 @@ enum SiriIntentFeature: CaseIterable, Hashable { case .todayRegions: .todayRegions case .daysInRegion: .daysInRegion case .regionOnDate: .regionOnDate - case .recentActivity: .recentActivity case .logDay: .logDay case .logTrip: .logTrip } @@ -27,7 +25,6 @@ enum SiriIntentFeature: CaseIterable, Hashable { case .todayRegions: "location.fill" case .daysInRegion: "calendar" case .regionOnDate: "calendar.badge.clock" - case .recentActivity: "sparkles" case .logDay: "mappin.and.ellipse" case .logTrip: "airplane" } @@ -38,7 +35,6 @@ enum SiriIntentFeature: CaseIterable, Hashable { case .todayRegions: String(localized: .settingsExploreSiriTodayRequest) case .daysInRegion: String(localized: .settingsExploreSiriDaysRequest) case .regionOnDate: String(localized: .settingsExploreSiriDateRequest) - case .recentActivity: String(localized: .settingsExploreSiriRecentRequest) case .logDay: String(localized: .settingsExploreSiriLogDayRequest) case .logTrip: String(localized: .settingsExploreSiriLogTripRequest) } @@ -49,7 +45,6 @@ enum SiriIntentFeature: CaseIterable, Hashable { case .todayRegions: String(localized: .settingsExploreSiriTodayResponse) case .daysInRegion: String(localized: .settingsExploreSiriDaysResponse) case .regionOnDate: String(localized: .settingsExploreSiriDateResponse) - case .recentActivity: String(localized: .settingsExploreSiriRecentResponse) case .logDay: String(localized: .settingsExploreSiriLogDayResponse) case .logTrip: String(localized: .settingsExploreSiriLogTripResponse) } diff --git a/Where/WhereUI/Sources/Settings/SettingsView.swift b/Where/WhereUI/Sources/Settings/SettingsView.swift index 4cb96e1a1..e4d214e1b 100644 --- a/Where/WhereUI/Sources/Settings/SettingsView.swift +++ b/Where/WhereUI/Sources/Settings/SettingsView.swift @@ -258,7 +258,6 @@ struct SettingsView: View { InsightsAccuracyFeaturesView( report: report, focus: route.focus, - presentation: featureDiscoveryPresentation, ) case .personalization: PersonalizationFeaturesView(report: report, focus: route.focus) diff --git a/Where/WhereUI/Sources/Shared/AppIconActivityIndicator.swift b/Where/WhereUI/Sources/Shared/AppIconActivityIndicator.swift index 4833333a4..6a6fba736 100644 --- a/Where/WhereUI/Sources/Shared/AppIconActivityIndicator.swift +++ b/Where/WhereUI/Sources/Shared/AppIconActivityIndicator.swift @@ -4,8 +4,7 @@ import SwiftUI /// gentle breathing pulse and a soft brand-tinted glow. A quieter cousin of the /// launch splash's pulsing hero (`LaunchSplashView`): smaller, with a shallower /// scale pulse, a softer glow, and none of the radar sweep — tuned for an -/// in-app wait such as generating the recent-activity summary rather than a -/// full-screen launch. +/// in-app wait rather than a full-screen launch. /// /// Honors Reduce Motion: the pulse pins to a static frame. Snapshot captures /// pin it the same way (see ``MotionIsStatic``). diff --git a/Where/WhereUI/Sources/Shared/TypewriterSchedule.swift b/Where/WhereUI/Sources/Shared/TypewriterSchedule.swift deleted file mode 100644 index a22c8ce80..000000000 --- a/Where/WhereUI/Sources/Shared/TypewriterSchedule.swift +++ /dev/null @@ -1,39 +0,0 @@ -import Foundation - -/// Pure pacing for a typewriter-style reveal: a base delay between characters, -/// with a longer beat after sentence-ending punctuation so streamed text lands -/// in natural sentences rather than a uniform crawl. Kept free of any view or -/// clock so the cadence is unit-tested deterministically, without waiting on -/// wall-clock time. -enum TypewriterSchedule { - /// Punctuation that ends a sentence and earns a pause after it. - static let sentenceTerminators: Set = [".", "!", "?", "…"] - - /// Whether the reveal should pause after the character at `index`: true only - /// when it's a sentence terminator followed by whitespace or the end of the - /// text. The "followed by whitespace" guard keeps mid-token dots quiet — a - /// decimal like "3.14" or an abbreviation like "e.g.x" won't trigger a beat, - /// while "Done. Next" does. - static func pausesAfter(index: Int, in characters: [Character]) -> Bool { - guard characters.indices.contains(index), - sentenceTerminators.contains(characters[index]) - else { - return false - } - let next = index + 1 - guard next < characters.count else { return true } - return characters[next].isWhitespace - } - - /// The delay to wait after revealing the character at `index` before - /// revealing the next one: `sentencePause` at a sentence boundary, otherwise - /// the base `characterDelay`. - static func delay( - afterIndex index: Int, - in characters: [Character], - characterDelay: Duration, - sentencePause: Duration, - ) -> Duration { - pausesAfter(index: index, in: characters) ? sentencePause : characterDelay - } -} diff --git a/Where/WhereUI/Sources/Shared/TypewriterText.swift b/Where/WhereUI/Sources/Shared/TypewriterText.swift deleted file mode 100644 index 902505db8..000000000 --- a/Where/WhereUI/Sources/Shared/TypewriterText.swift +++ /dev/null @@ -1,68 +0,0 @@ -import SwiftUI - -/// Reveals `text` one character at a time, the way conversational AI tools -/// stream a reply — pausing a beat at sentence boundaries (see -/// `TypewriterSchedule`). Style it like any `Text`: font, color, and frame -/// modifiers applied to this view flow through to the rendered characters. -/// -/// The full text is always exposed to assistive technologies (VoiceOver reads -/// the whole summary, not the partial reveal), and Reduce Motion shows it all -/// at once. Snapshot captures also render the final, fully revealed text — the -/// reveal's end state (see ``MotionIsStatic``). Restarts cleanly when `text` -/// changes. -struct TypewriterText: View { - let text: String - /// Base cadence between characters. - var characterDelay: Duration = .milliseconds(18) - /// Extra beat held at a sentence boundary. - var sentencePause: Duration = .milliseconds(340) - - @MotionIsStatic private var motionIsStatic - @State private var revealedCount = 0 - - var body: some View { - let characters = Array(text) - Text(String(characters.prefix(revealedCount))) - .accessibilityLabel(text) - .task(id: text) { await reveal(characters) } - } - - /// Walk the characters, revealing one per step and sleeping the scheduled - /// delay between them. Static motion (Reduce Motion or snapshot capture — - /// see ``MotionIsStatic``) and an empty string reveal instantly. - /// Cancellation — the view went away or `text` changed — stops quietly; a - /// new `text` restarts this task from zero. - private func reveal(_ characters: [Character]) async { - guard !motionIsStatic else { - revealedCount = characters.count - return - } - revealedCount = 0 - for index in characters.indices { - revealedCount = index + 1 - guard index + 1 < characters.count else { break } - do { - try await Task.sleep( - for: TypewriterSchedule.delay( - afterIndex: index, - in: characters, - characterDelay: characterDelay, - sentencePause: sentencePause, - ), - ) - } catch { - return - } - } - } -} - -#if DEBUG - #Preview { - TypewriterText( - text: "You spent the morning in California. In the early evening you traveled to New York, where the most recent readings place you.", - ) - .font(.body) - .padding() - } -#endif diff --git a/Where/WhereUI/Sources/Shared/WhereFormat.swift b/Where/WhereUI/Sources/Shared/WhereFormat.swift index a8e489305..719ce11e1 100644 --- a/Where/WhereUI/Sources/Shared/WhereFormat.swift +++ b/Where/WhereUI/Sources/Shared/WhereFormat.swift @@ -321,59 +321,6 @@ enum WhereFormat { } } - // MARK: Recent activity - - static func recentActivityTitle(_ window: RecentActivityWindow) -> String { - switch window { - case .day: String(localized: .recentActivityTitleDay) - case .week: String(localized: .recentActivityTitleWeek) - case .month: String(localized: .recentActivityTitleMonth) - case .yearToDate: String(localized: .recentActivityTitleYearToDate) - } - } - - static func recentActivityWindowLabel(_ window: RecentActivityWindow) -> String { - switch window { - case .day: String(localized: .recentActivityWindowDay) - case .week: String(localized: .recentActivityWindowWeek) - case .month: String(localized: .recentActivityWindowMonth) - case .yearToDate: String(localized: .recentActivityWindowYearToDate) - } - } - - static func recentActivityFooter(_ window: RecentActivityWindow) -> String { - switch window { - case .day: String(localized: .recentActivityFooterDay) - case .week: String(localized: .recentActivityFooterWeek) - case .month: String(localized: .recentActivityFooterMonth) - case .yearToDate: String(localized: .recentActivityFooterYearToDate) - } - } - - static func recentActivityEmptyDescription(_ window: RecentActivityWindow) -> String { - switch window { - case .day: String(localized: .recentActivityEmptyDescriptionDay) - case .week: String(localized: .recentActivityEmptyDescriptionWeek) - case .month: String(localized: .recentActivityEmptyDescriptionMonth) - case .yearToDate: String(localized: .recentActivityEmptyDescriptionYearToDate) - } - } - - static func recentActivityUnavailableMessage( - _ reason: ActivitySummaryUnavailableReason, - ) -> String { - switch reason { - case .deviceNotEligible: - String(localized: .recentActivityUnavailableDeviceNotEligible) - case .appleIntelligenceNotEnabled: - String(localized: .recentActivityUnavailableAppleIntelligenceNotEnabled) - case .modelNotReady: - String(localized: .recentActivityUnavailableModelNotReady) - case .unknown: - String(localized: .recentActivityUnavailableUnknown) - } - } - // MARK: About /// A build-metadata value, or a localized "Unknown" when the bundle doesn't diff --git a/Where/WhereUI/Sources/Year/YearView.swift b/Where/WhereUI/Sources/Year/YearView.swift index 16365067d..0026c4f1b 100644 --- a/Where/WhereUI/Sources/Year/YearView.swift +++ b/Where/WhereUI/Sources/Year/YearView.swift @@ -4,13 +4,11 @@ import WhereCore /// Your Year tab: the selected year's calendar and timeline for the same data. /// A floating Liquid Glass pill at the bottom (Photos-style) zooms between the -/// calendar (month detail) and the timeline (year overview); the activity -/// summary sits in the toolbar. +/// calendar (month detail) and the timeline (year overview). struct YearView: View { let report: YearReportModel @State private var mode: YearMode - @State private var showingRecentActivity = false @Environment(\.stylesheet) private var stylesheet @@ -42,19 +40,6 @@ struct YearView: View { YearModePicker(mode: $mode) .padding(.bottom, stylesheet.spacing.xLarge) } - .toolbar { - ToolbarItem(placement: .topBarLeading) { - Button { - showingRecentActivity = true - } label: { - Label(String(localized: .primaryRecentActivity), systemImage: "sparkles") - } - .accessibilityIdentifier("where_recent_activity_button") - } - } - } - .sheet(isPresented: $showingRecentActivity) { - RecentActivitySummaryView(report: report) } } } @@ -166,9 +151,6 @@ private struct YearModePicker: View { extension YearView: WhereFlyoverProviding { static let flyoverData = WhereFlyoverData( YearView.self, - routes: [ - .modal(to: RecentActivitySummaryView.flyoverID), - ], ) { id, world in .init( id: id, diff --git a/Where/WhereUI/Tests/FeatureDiscoveryPresentationTests.swift b/Where/WhereUI/Tests/FeatureDiscoveryPresentationTests.swift index 904123d3a..39438aae6 100644 --- a/Where/WhereUI/Tests/FeatureDiscoveryPresentationTests.swift +++ b/Where/WhereUI/Tests/FeatureDiscoveryPresentationTests.swift @@ -26,8 +26,6 @@ struct FeatureDiscoveryPresentationTests { #expect(presentation.widgetSnapshot.dayRegions.isEmpty) #expect(presentation.siriExample(for: .daysInRegion) == nil) #expect(presentation.spotlightExample.query == Region.california.localizedName) - #expect(presentation.activityExample - .summary == String(localized: .settingsExploreInsightsActivityGeneric)) } @Test func twoWeeksPersonalizesContent() throws { @@ -47,7 +45,6 @@ struct FeatureDiscoveryPresentationTests { ) let daysExample = try #require(presentation.siriExample(for: .daysInRegion)) let todayExample = try #require(presentation.siriExample(for: .todayRegions)) - let recentExample = try #require(presentation.siriExample(for: .recentActivity)) #expect(presentation.usesUserData) #expect(presentation.widgetSnapshot.year == 2026) @@ -56,40 +53,8 @@ struct FeatureDiscoveryPresentationTests { #expect(daysExample.response.contains(Region.california.localizedName)) #expect(daysExample.response.contains("14")) #expect(todayExample.response.contains(Region.california.localizedName)) - #expect(recentExample.request == String(localized: .settingsExploreSiriRecentRequest)) - #expect(recentExample.response.contains(Region.california.localizedName)) #expect(presentation.spotlightExample.query == Region.california.localizedName) #expect(presentation.spotlightExample.resultSubtitle.contains("14")) - #expect(presentation.activityExample.summary.contains("14")) - #expect(presentation.activityExample.summary.contains(Region.california.localizedName)) - } - - @Test func recentActivityUsesTheIntentsPastWeekWindow() throws { - let calendar = calendar() - let referenceDate = try referenceDate(in: calendar) - let days = (0 ..< FeatureDiscoveryPresentation.minimumLoggedDayCount).map { offset in - DayPresence( - day: CalendarDay(year: 2026, month: 7, day: 26).adding(days: offset), - regions: offset < 6 ? [.california] : [.newYork], - ) - } - let report = YearReport( - year: 2026, - days: days, - totals: [.california: 6, .newYork: 8], - ) - - let presentation = FeatureDiscoveryPresentation( - report: report, - selectedYear: report.year, - referenceDate: referenceDate, - calendar: calendar, - ) - let recentExample = try #require(presentation.siriExample(for: .recentActivity)) - - #expect(recentExample.request == String(localized: .settingsExploreSiriRecentRequest)) - #expect(recentExample.response.contains(Region.newYork.localizedName)) - #expect(recentExample.response.contains(Region.california.localizedName) == false) } @Test func futureDaysDoNotUnlockPersonalization() throws { diff --git a/Where/WhereUI/Tests/RecentActivityModelTests.swift b/Where/WhereUI/Tests/RecentActivityModelTests.swift deleted file mode 100644 index d5d68128c..000000000 --- a/Where/WhereUI/Tests/RecentActivityModelTests.swift +++ /dev/null @@ -1,136 +0,0 @@ -import Foundation -import RegionKit -import Testing -@_spi(Testing) import WhereCore -@testable import WhereUI - -/// Covers `RecentActivityModel`'s mapping of the summarizer's output (and its -/// failures) to a `LoadState`, against a scripted generator and an in-memory -/// store. -@MainActor -struct RecentActivityModelTests { - private enum StubError: Error { case boom } - - /// Scripted `ActivitySummaryGenerating`. Immutable, so `Sendable`. - private struct StubGenerator: ActivitySummaryGenerating { - enum Outcome { - case text(String) - case unavailable(ActivitySummaryUnavailableReason) - case failure - } - - let outcome: Outcome - - func summarize(_: RecentActivityInput) async throws -> String { - switch outcome { - case let .text(text): return text - case let .unavailable(reason): throw ActivitySummaryUnavailableError(reason: reason) - case .failure: throw StubError.boom - } - } - } - - /// `nonisolated` so the `@Sendable` `now:` closure in `makeServices` can - /// capture it without hopping off this `@MainActor` suite; `Date` is `Sendable`. - private nonisolated static let now = Calendar.current.date( - from: DateComponents(year: 2026, month: 5, day: 2, hour: 12), - )! - - private func makeServices(store: TestStore, generator: StubGenerator) -> WhereServices { - WhereServices( - store: store, - locationSource: ScriptedLocationSource(), - reminderScheduler: NoopLoggingReminderScheduler(), - widgetRefresher: NoopWidgetTimelineRefresher(), - activitySummaryGenerator: generator, - now: { Self.now }, - ) - } - - private func seedRecentSample(_ services: WhereServices) async throws { - try await services.journal.ingest(LocationSample( - timestamp: Self.now.addingTimeInterval(-3600), - coordinate: Coordinate(latitude: 37.7749, longitude: -122.4194), - horizontalAccuracy: 5, - source: .gpsSignificantChange, - )) - } - - @Test func loadWithNoSamplesIsEmpty() async throws { - let store = try TestStore() - let services = makeServices( - store: store, - generator: StubGenerator(outcome: .text("unused")), - ) - let model = RecentActivityModel(services: services) - - await model.load() - - #expect(model.loadState == .empty) - } - - @Test func loadWithSamplesShowsGeneratedSummary() async throws { - let store = try TestStore() - let services = makeServices( - store: store, - generator: StubGenerator(outcome: .text("You spent the day in California.")), - ) - try await seedRecentSample(services) - let model = RecentActivityModel(services: services) - - await model.load() - - #expect(model.loadState == .loaded("You spent the day in California.")) - } - - @Test func loadMapsUnavailableModelToUnavailableState() async throws { - let store = try TestStore() - let services = makeServices( - store: store, - generator: StubGenerator(outcome: .unavailable(.appleIntelligenceNotEnabled)), - ) - try await seedRecentSample(services) - let model = RecentActivityModel(services: services) - - await model.load() - - #expect(model.loadState == .unavailable(.appleIntelligenceNotEnabled)) - } - - @Test func loadMapsGenerationFailureToFailedState() async throws { - let store = try TestStore() - let services = makeServices(store: store, generator: StubGenerator(outcome: .failure)) - try await seedRecentSample(services) - let model = RecentActivityModel(services: services) - - await model.load() - - guard case .failed = model.loadState else { - Issue.record("Expected .failed, got \(model.loadState)") - return - } - } - - @Test func changingWindowRegeneratesForTheNewRange() async throws { - let store = try TestStore() - let services = makeServices( - store: store, - generator: StubGenerator(outcome: .text("You were around.")), - ) - // A reading three days ago is outside `.day` but inside `.week`. - try await services.journal.ingest(LocationSample( - timestamp: Self.now.addingTimeInterval(-3 * 24 * 3600), - coordinate: Coordinate(latitude: 37.7749, longitude: -122.4194), - horizontalAccuracy: 5, - source: .gpsSignificantChange, - )) - let model = RecentActivityModel(services: services) - - await model.load() - #expect(model.loadState == .empty) - - model.window = .week - await model.load() - #expect(model.loadState == .loaded("You were around.")) - } -} diff --git a/Where/WhereUI/Tests/TypewriterScheduleTests.swift b/Where/WhereUI/Tests/TypewriterScheduleTests.swift deleted file mode 100644 index a5adc0ae7..000000000 --- a/Where/WhereUI/Tests/TypewriterScheduleTests.swift +++ /dev/null @@ -1,58 +0,0 @@ -import Foundation -import Testing -@testable import WhereUI - -/// Covers `TypewriterSchedule`'s pause logic: a beat lands after sentence -/// terminators that end a sentence, but not after mid-token punctuation, and -/// `delay` picks the sentence pause only at those boundaries. -struct TypewriterScheduleTests { - private static let base = Duration.milliseconds(10) - private static let pause = Duration.milliseconds(200) - - @Test func pausesAfterASentenceTerminatorFollowedByASpace() { - let characters = Array("Done. Next") - // "Done." — the period is at index 4, followed by a space. - #expect(TypewriterSchedule.pausesAfter(index: 4, in: characters)) - } - - @Test func pausesAfterATerminatorAtTheEndOfText() { - let characters = Array("Hello.") - #expect(TypewriterSchedule.pausesAfter(index: 5, in: characters)) - } - - @Test func doesNotPauseOnAMidNumberDot() { - let characters = Array("3.14") - // The dot at index 1 is followed by "1", not whitespace. - #expect(!TypewriterSchedule.pausesAfter(index: 1, in: characters)) - } - - @Test func doesNotPauseOnANonTerminatorCharacter() { - let characters = Array("Done. Next") - #expect(!TypewriterSchedule.pausesAfter(index: 0, in: characters)) - } - - @Test func pausesAfterQuestionAndExclamationMarks() { - let question = Array("Why? Because") - let exclamation = Array("Go! Now") - #expect(TypewriterSchedule.pausesAfter(index: 3, in: question)) - #expect(TypewriterSchedule.pausesAfter(index: 2, in: exclamation)) - } - - @Test func delayUsesTheSentencePauseOnlyAtABoundary() { - let characters = Array("Hi. Go") - let atBoundary = TypewriterSchedule.delay( - afterIndex: 2, - in: characters, - characterDelay: Self.base, - sentencePause: Self.pause, - ) - let midText = TypewriterSchedule.delay( - afterIndex: 0, - in: characters, - characterDelay: Self.base, - sentencePause: Self.pause, - ) - #expect(atBoundary == Self.pause) - #expect(midText == Self.base) - } -} From 0930d94c6567bc4f53073945a579813b7972e5e7 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sun, 9 Aug 2026 19:26:25 -0700 Subject: [PATCH 2/3] docs(Where): remove stale AI summary references --- Where/WhereCore/README.md | 2 +- Where/WhereIntents/README.md | 3 +-- Where/WhereUI/README.md | 19 +++++++++---------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/Where/WhereCore/README.md b/Where/WhereCore/README.md index e713e96be..773bcd78e 100644 --- a/Where/WhereCore/README.md +++ b/Where/WhereCore/README.md @@ -119,7 +119,7 @@ one it belongs to rather than to a god-object: target-owned advisory check-ins, and global removal tombstones sync independently. Another installation can rename or remove a device identity, but cannot change its recording consent. - **`LocationHistoryReader`** — the shared removal-aware read boundary used by reports, widgets, - recent activity, and foreground capture checks. It hides a removed identity's GPS samples at + and foreground capture checks. It hides a removed identity's GPS samples at and after its earliest tombstone while keeping earlier raw storage, backups, legacy samples without provenance, and user-asserted samples lossless. diff --git a/Where/WhereIntents/README.md b/Where/WhereIntents/README.md index 514a4df86..f7c684027 100644 --- a/Where/WhereIntents/README.md +++ b/Where/WhereIntents/README.md @@ -71,8 +71,7 @@ Every intent's work is one budgeted Periscope span named after the intent (`perform(days-in-region)`), so a Siri answer that felt slow can be attributed to the read, the write, or the wait itself: `IntentServices.current()` spans only the path where it actually parks for the app's launch to install the services -stack. Budgets live on `WhereIntentsLog.IntentName` beside the name, and the -slow-by-nature intents (the on-device model summary) get the slack. +stack. Budgets live on `WhereIntentsLog.IntentName` beside the name. ## Localization diff --git a/Where/WhereUI/README.md b/Where/WhereUI/README.md index a0e1bf9ac..c357513e7 100644 --- a/Where/WhereUI/README.md +++ b/Where/WhereUI/README.md @@ -24,19 +24,18 @@ the feature [`Where/AGENTS.md`](../AGENTS.md) and this module's `MainTabs`, the Liquid Glass tab bar over three tabs — Locations, Your Year, Settings. Elsewhere is an entry card on Locations, Resolve a Locations toolbar button, and the data screens (attachments, logged days, regions) sit in the - Settings "Data" group. The **Explore Features** group demonstrates all six - Siri/Shortcuts intent as an example conversation, demonstrates tracked-region + Settings "Data" group. The **Explore Features** group demonstrates all five + Siri/Shortcuts intents as example conversations, demonstrates tracked-region results in Spotlight, and renders every supported widget family on miniature Home Screen and Lock Screen surfaces. A Share & Evidence walkthrough also reveals the system Share-sheet extension and links - into the saved attachment archive. Insights & Accuracy introduces the private - on-device activity narrative and the automatic issue detectors without - running either merely to render the gallery. These galleries use a - shared marketing header, quiet patterned backdrop, and staged entrance that - resolves immediately for Reduce Motion and snapshot capture. Once the selected - report has 14 recorded days, both galleries personalize themselves with its - real regions, counts, and dates; sparse reports keep the illustrative Siri copy - and empty widget state. + into the saved attachment archive. Insights & Accuracy introduces the + automatic issue detectors without running them merely to render the gallery. + These galleries use a shared marketing header, quiet patterned backdrop, and + staged entrance that resolves immediately for Reduce Motion and snapshot + capture. Once the selected report has 14 recorded days, the Siri, Spotlight, + widget, and evidence examples use its real regions, counts, and dates; sparse + reports keep the illustrative Siri copy and empty widget state. Backup and destructive data management share one Data drill-in. Both Data and About lead with the same full-width passport-style privacy statement on a passport-navy, tilt-reflective surface: location From e0a78e5dbb9d96b0ba1bb3fbeadbbec12e66660b Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sun, 9 Aug 2026 20:19:59 -0700 Subject: [PATCH 3/3] test(WhereUI): refresh flyover catalog snapshot --- .../seededEntryState.WhereFlyover_iPad.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereFlyoverViewSnapshotTests/seededEntryState.WhereFlyover_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereFlyoverViewSnapshotTests/seededEntryState.WhereFlyover_iPad.png index 3f83d374b..f04db519b 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereFlyoverViewSnapshotTests/seededEntryState.WhereFlyover_iPad.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereFlyoverViewSnapshotTests/seededEntryState.WhereFlyover_iPad.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e85cb3902204d283b38faab601291f97e3c495aa899b33fd31127871b18a151d -size 9651117 +oid sha256:f1483d0f964015020e672d47e19f5c86ab39315f714f33524b701510a4107046 +size 9416258