@adminide-stack/yantra-mobile 12.0.141-alpha.1 → 12.0.141

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/lib/features/agent-chat/NativeAgentChat.js +21 -4
  2. package/lib/features/agent-chat/NativeAgentChat.js.map +1 -1
  3. package/lib/features/chat/ApprovalCard.js +353 -0
  4. package/lib/features/chat/ApprovalCard.js.map +1 -0
  5. package/lib/features/chat/AskUserCard.js +9 -2
  6. package/lib/features/chat/AskUserCard.js.map +1 -1
  7. package/lib/features/chat/askUser.js +25 -3
  8. package/lib/features/chat/askUser.js.map +1 -1
  9. package/lib/features/chat/queuedFollowUpRows.js +13 -0
  10. package/lib/features/chat/queuedFollowUpRows.js.map +1 -0
  11. package/lib/features/devices/bodyComposition.js +116 -0
  12. package/lib/features/devices/bodyComposition.js.map +1 -0
  13. package/lib/features/devices/readings.js.map +1 -1
  14. package/lib/features/devices/session.js +22 -2
  15. package/lib/features/devices/session.js.map +1 -1
  16. package/lib/features/discover/DiscoverArticle.js +105 -92
  17. package/lib/features/discover/DiscoverArticle.js.map +1 -1
  18. package/lib/features/discover/DiscoverFeed.js +97 -48
  19. package/lib/features/discover/DiscoverFeed.js.map +1 -1
  20. package/lib/features/discover/DiscoverFollowUp.js +135 -0
  21. package/lib/features/discover/DiscoverFollowUp.js.map +1 -0
  22. package/lib/features/discover/DiscoverSidebar.js +555 -0
  23. package/lib/features/discover/DiscoverSidebar.js.map +1 -0
  24. package/lib/features/discover/DiscoverSkeletons.js +123 -0
  25. package/lib/features/discover/DiscoverSkeletons.js.map +1 -0
  26. package/lib/features/discover/DiscoverStoryCards.js +193 -87
  27. package/lib/features/discover/DiscoverStoryCards.js.map +1 -1
  28. package/lib/features/discover/followUpQuery.js +42 -0
  29. package/lib/features/discover/followUpQuery.js.map +1 -0
  30. package/lib/features/discover/markets.js +37 -0
  31. package/lib/features/discover/markets.js.map +1 -0
  32. package/lib/features/discover/useDiscoverSidebar.js +148 -0
  33. package/lib/features/discover/useDiscoverSidebar.js.map +1 -0
  34. package/lib/features/discover/weather.js +42 -0
  35. package/lib/features/discover/weather.js.map +1 -0
  36. package/lib/features/expenses/ExpensesAgentChatPane.js +1 -1
  37. package/lib/features/expenses/ExpensesAgentChatPane.js.map +1 -1
  38. package/lib/features/inbox/InboxAgentChatPane.js +1 -1
  39. package/lib/features/inbox/InboxAgentChatPane.js.map +1 -1
  40. package/lib/features/job-search/JobSearchAgentChatPane.js +1 -0
  41. package/lib/features/job-search/JobSearchAgentChatPane.js.map +1 -1
  42. package/lib/features/local-services/LocalServicesAgentChatPane.js +1 -1
  43. package/lib/features/local-services/LocalServicesAgentChatPane.js.map +1 -1
  44. package/lib/features/movies/MoviesAgentChatPane.js +1 -1
  45. package/lib/features/movies/MoviesAgentChatPane.js.map +1 -1
  46. package/lib/features/notion/NotionBlockSheets.js +206 -0
  47. package/lib/features/notion/NotionBlockSheets.js.map +1 -0
  48. package/lib/features/notion/NotionBlockView.js +36 -0
  49. package/lib/features/notion/NotionBlockView.js.map +1 -0
  50. package/lib/features/notion/NotionCommentsSheet.js +461 -0
  51. package/lib/features/notion/NotionCommentsSheet.js.map +1 -0
  52. package/lib/features/notion/NotionConnectionsSheet.js +244 -0
  53. package/lib/features/notion/NotionConnectionsSheet.js.map +1 -0
  54. package/lib/features/notion/NotionHistorySheet.js +439 -0
  55. package/lib/features/notion/NotionHistorySheet.js.map +1 -0
  56. package/lib/features/notion/NotionMediaBlocks.js +803 -0
  57. package/lib/features/notion/NotionMediaBlocks.js.map +1 -0
  58. package/lib/features/notion/NotionMentionSheet.js +279 -0
  59. package/lib/features/notion/NotionMentionSheet.js.map +1 -0
  60. package/lib/features/notion/NotionPageMenu.js +585 -0
  61. package/lib/features/notion/NotionPageMenu.js.map +1 -0
  62. package/lib/features/notion/NotionPageTree.js +220 -0
  63. package/lib/features/notion/NotionPageTree.js.map +1 -0
  64. package/lib/features/notion/NotionPageView.js +1253 -152
  65. package/lib/features/notion/NotionPageView.js.map +1 -1
  66. package/lib/features/notion/NotionShareSheet.js +606 -0
  67. package/lib/features/notion/NotionShareSheet.js.map +1 -0
  68. package/lib/features/notion/NotionSheets.js +87 -15
  69. package/lib/features/notion/NotionSheets.js.map +1 -1
  70. package/lib/features/notion/NotionSidebarSections.js +492 -0
  71. package/lib/features/notion/NotionSidebarSections.js.map +1 -0
  72. package/lib/features/notion/NotionWorkspaceAdmin.js +659 -0
  73. package/lib/features/notion/NotionWorkspaceAdmin.js.map +1 -0
  74. package/lib/features/notion/NotionWorkspaceView.js +257 -45
  75. package/lib/features/notion/NotionWorkspaceView.js.map +1 -1
  76. package/lib/features/notion/database/DatabaseCells.js +554 -0
  77. package/lib/features/notion/database/DatabaseCells.js.map +1 -0
  78. package/lib/features/notion/database/DatabaseDateLayouts.js +792 -0
  79. package/lib/features/notion/database/DatabaseDateLayouts.js.map +1 -0
  80. package/lib/features/notion/database/DatabaseLayouts.js +461 -0
  81. package/lib/features/notion/database/DatabaseLayouts.js.map +1 -0
  82. package/lib/features/notion/database/DatabaseSheets.js +791 -0
  83. package/lib/features/notion/database/DatabaseSheets.js.map +1 -0
  84. package/lib/features/notion/database/NotionDatabaseView.js +817 -0
  85. package/lib/features/notion/database/NotionDatabaseView.js.map +1 -0
  86. package/lib/features/notion/database/RowPropertiesPanel.js +162 -0
  87. package/lib/features/notion/database/RowPropertiesPanel.js.map +1 -0
  88. package/lib/features/notion/database/chartData.js +89 -0
  89. package/lib/features/notion/database/chartData.js.map +1 -0
  90. package/lib/features/notion/database/chartView.js +112 -0
  91. package/lib/features/notion/database/chartView.js.map +1 -0
  92. package/lib/features/notion/database/connectorButton.js +33 -0
  93. package/lib/features/notion/database/connectorButton.js.map +1 -0
  94. package/lib/features/notion/database/databaseCalc.js +208 -0
  95. package/lib/features/notion/database/databaseCalc.js.map +1 -0
  96. package/lib/features/notion/database/databaseCalendar.js +157 -0
  97. package/lib/features/notion/database/databaseCalendar.js.map +1 -0
  98. package/lib/features/notion/database/databaseCsv.js +98 -0
  99. package/lib/features/notion/database/databaseCsv.js.map +1 -0
  100. package/lib/features/notion/database/databaseCsvActions.js +58 -0
  101. package/lib/features/notion/database/databaseCsvActions.js.map +1 -0
  102. package/lib/features/notion/database/databaseFilter.js +473 -0
  103. package/lib/features/notion/database/databaseFilter.js.map +1 -0
  104. package/lib/features/notion/database/databaseFormula.js +1072 -0
  105. package/lib/features/notion/database/databaseFormula.js.map +1 -0
  106. package/lib/features/notion/database/databaseGroup.js +151 -0
  107. package/lib/features/notion/database/databaseGroup.js.map +1 -0
  108. package/lib/features/notion/database/databaseRollup.js +58 -0
  109. package/lib/features/notion/database/databaseRollup.js.map +1 -0
  110. package/lib/features/notion/database/databaseTimeline.js +156 -0
  111. package/lib/features/notion/database/databaseTimeline.js.map +1 -0
  112. package/lib/features/notion/database/databaseView.js +1188 -0
  113. package/lib/features/notion/database/databaseView.js.map +1 -0
  114. package/lib/features/notion/database/databaseViews.js +310 -0
  115. package/lib/features/notion/database/databaseViews.js.map +1 -0
  116. package/lib/features/notion/database/relationSource.js +92 -0
  117. package/lib/features/notion/database/relationSource.js.map +1 -0
  118. package/lib/features/notion/database/uniqueId.js +17 -0
  119. package/lib/features/notion/database/uniqueId.js.map +1 -0
  120. package/lib/features/notion/database/useNotionDatabase.js +617 -0
  121. package/lib/features/notion/database/useNotionDatabase.js.map +1 -0
  122. package/lib/features/notion/notionAgentRunStatus.js +39 -0
  123. package/lib/features/notion/notionAgentRunStatus.js.map +1 -0
  124. package/lib/features/notion/notionCodeLanguages.js +112 -0
  125. package/lib/features/notion/notionCodeLanguages.js.map +1 -0
  126. package/lib/features/notion/notionComments.js +86 -0
  127. package/lib/features/notion/notionComments.js.map +1 -0
  128. package/lib/features/notion/notionConnections.js +74 -0
  129. package/lib/features/notion/notionConnections.js.map +1 -0
  130. package/lib/features/notion/notionDocuments.js +453 -1
  131. package/lib/features/notion/notionDocuments.js.map +1 -1
  132. package/lib/features/notion/notionEditorModel.js +142 -7
  133. package/lib/features/notion/notionEditorModel.js.map +1 -1
  134. package/lib/features/notion/notionMarkdownShortcuts.js +51 -0
  135. package/lib/features/notion/notionMarkdownShortcuts.js.map +1 -0
  136. package/lib/features/notion/notionMedia.js +119 -0
  137. package/lib/features/notion/notionMedia.js.map +1 -0
  138. package/lib/features/notion/notionMentionDate.js +32 -0
  139. package/lib/features/notion/notionMentionDate.js.map +1 -0
  140. package/lib/features/notion/notionMentionOptions.js +162 -0
  141. package/lib/features/notion/notionMentionOptions.js.map +1 -0
  142. package/lib/features/notion/notionNotesAgent.js +445 -0
  143. package/lib/features/notion/notionNotesAgent.js.map +1 -0
  144. package/lib/features/notion/notionPageAccess.js +168 -0
  145. package/lib/features/notion/notionPageAccess.js.map +1 -0
  146. package/lib/features/notion/notionPageActions.js +138 -0
  147. package/lib/features/notion/notionPageActions.js.map +1 -0
  148. package/lib/features/notion/notionPageHistory.js +154 -0
  149. package/lib/features/notion/notionPageHistory.js.map +1 -0
  150. package/lib/features/notion/notionPageKinds.js +200 -0
  151. package/lib/features/notion/notionPageKinds.js.map +1 -0
  152. package/lib/features/notion/notionRecents.js +48 -0
  153. package/lib/features/notion/notionRecents.js.map +1 -0
  154. package/lib/features/notion/notionRichText.js +177 -0
  155. package/lib/features/notion/notionRichText.js.map +1 -0
  156. package/lib/features/notion/notionRichTitle.js +336 -0
  157. package/lib/features/notion/notionRichTitle.js.map +1 -0
  158. package/lib/features/notion/useNotionWorkspace.js +272 -13
  159. package/lib/features/notion/useNotionWorkspace.js.map +1 -1
  160. package/lib/features/notion/usePageActions.js +155 -0
  161. package/lib/features/notion/usePageActions.js.map +1 -0
  162. package/lib/features/resume/ResumeAgentChatPane.js +1 -1
  163. package/lib/features/resume/ResumeAgentChatPane.js.map +1 -1
  164. package/lib/features/shopping/ShoppingAgentChatPane.js +1 -1
  165. package/lib/features/shopping/ShoppingAgentChatPane.js.map +1 -1
  166. package/lib/features/study/StudyAgentChatPane.js +1 -1
  167. package/lib/features/study/StudyAgentChatPane.js.map +1 -1
  168. package/lib/features/travel/TravelAgentChatPane.js +1 -1
  169. package/lib/features/travel/TravelAgentChatPane.js.map +1 -1
  170. package/lib/features/webbuilder/WebBuilderExperience.js +40 -8
  171. package/lib/features/webbuilder/WebBuilderExperience.js.map +1 -1
  172. package/lib/features/webbuilder/buildFragments.js +1 -1
  173. package/lib/features/webbuilder/buildFragments.js.map +1 -1
  174. package/lib/features/webbuilder/codeFiles.js +302 -0
  175. package/lib/features/webbuilder/codeFiles.js.map +1 -0
  176. package/lib/features/webbuilder/codeHighlight.js +209 -0
  177. package/lib/features/webbuilder/codeHighlight.js.map +1 -0
  178. package/lib/features/webbuilder/components/CodeView.js +682 -52
  179. package/lib/features/webbuilder/components/CodeView.js.map +1 -1
  180. package/lib/features/webbuilder/components/PreviewScreen.js +59 -38
  181. package/lib/features/webbuilder/components/PreviewScreen.js.map +1 -1
  182. package/lib/features/webbuilder/components/ProjectChat.js +3 -3
  183. package/lib/features/webbuilder/components/ProjectChat.js.map +1 -1
  184. package/lib/features/webbuilder/stepLabels.js +68 -0
  185. package/lib/features/webbuilder/stepLabels.js.map +1 -0
  186. package/lib/hooks/useHostChatSteering.js +45 -0
  187. package/lib/hooks/useHostChatSteering.js.map +1 -0
  188. package/lib/screens/Chat/index.js +2 -12
  189. package/lib/screens/Chat/index.js.map +1 -1
  190. package/lib/screens/Devices/index.js +205 -3
  191. package/lib/screens/Devices/index.js.map +1 -1
  192. package/lib/screens/ExpensesNative/useExpensesHostChat.js +25 -5
  193. package/lib/screens/ExpensesNative/useExpensesHostChat.js.map +1 -1
  194. package/lib/screens/InboxNative/useInboxHostChat.js +25 -5
  195. package/lib/screens/InboxNative/useInboxHostChat.js.map +1 -1
  196. package/lib/screens/JobSearchNative/useJobSearchHostChat.js +25 -5
  197. package/lib/screens/JobSearchNative/useJobSearchHostChat.js.map +1 -1
  198. package/lib/screens/LocalServicesNative/useLocalServicesHostChat.js +25 -5
  199. package/lib/screens/LocalServicesNative/useLocalServicesHostChat.js.map +1 -1
  200. package/lib/screens/MoviesNative/useMoviesHostChat.js +25 -5
  201. package/lib/screens/MoviesNative/useMoviesHostChat.js.map +1 -1
  202. package/lib/screens/NotionPage/index.js +2 -2
  203. package/lib/screens/NotionPage/index.js.map +1 -1
  204. package/lib/screens/ResumeNative/useResumeHostChat.js +25 -5
  205. package/lib/screens/ResumeNative/useResumeHostChat.js.map +1 -1
  206. package/lib/screens/ShoppingNative/useShoppingHostChat.js +25 -5
  207. package/lib/screens/ShoppingNative/useShoppingHostChat.js.map +1 -1
  208. package/lib/screens/StudyNative/useStudyHostChat.js +25 -5
  209. package/lib/screens/StudyNative/useStudyHostChat.js.map +1 -1
  210. package/lib/screens/TravelNative/useTravelHostChat.js +25 -5
  211. package/lib/screens/TravelNative/useTravelHostChat.js.map +1 -1
  212. package/lib/screens/WebBuilderNative/codeWorkspaceApi.js +222 -0
  213. package/lib/screens/WebBuilderNative/codeWorkspaceApi.js.map +1 -0
  214. package/lib/screens/WebBuilderNative/index.js +1 -1
  215. package/lib/screens/WebBuilderNative/index.js.map +1 -1
  216. package/lib/screens/WebBuilderNative/sandboxProxyApi.js +126 -4
  217. package/lib/screens/WebBuilderNative/sandboxProxyApi.js.map +1 -1
  218. package/lib/screens/WebBuilderNative/useWebBuilderHost.js +120 -11
  219. package/lib/screens/WebBuilderNative/useWebBuilderHost.js.map +1 -1
  220. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"readings.js","sources":["../../../src/features/devices/readings.ts"],"sourcesContent":["/**\n * What a paired Bluetooth device measured, and how it reaches the Fitness\n * backend.\n *\n * The backend keeps ONE day per source and replaces it whole on every upload,\n * so a reading is never sent alone: the app keeps every reading it took (the\n * device store), and each upload rebuilds the full `device` day for the dates\n * that changed. A blood-pressure reading in the evening never wipes the\n * morning's weigh-in.\n *\n * Reading ids are stable (device + what + when): a monitor that sends its\n * whole memory on every connection, or the same reading arriving twice, is\n * stored once.\n */\n\nimport { sanitizeDay, hasData } from '../health/buildDays';\nimport type { TimedValue } from '../health/model';\nimport type { HealthDay, HealthReading, HealthWorkout, MealRelation } from '../health/types';\nimport type { ZoneClock } from '../health/time';\nimport { zoneMinutes } from '../health/zones';\n\ninterface ReadingBase {\n id: string;\n /** Epoch ms of the measurement. */\n at: number;\n deviceId: string;\n deviceName: string;\n}\n\nexport interface BodyReading extends ReadingBase {\n kind: 'body';\n weightKg?: number;\n bodyFatPct?: number;\n leanMassKg?: number;\n bmi?: number;\n /**\n * Whole-body impedance in ohms, when the scale sends it. A scale that only\n * reports weight and impedance (RENPHO's common protocol) leaves body fat\n * to Yantra's own estimate; one that computes body fat itself (R-A017) also\n * fills bodyFatPct and bmi.\n */\n impedanceOhm?: number;\n}\n\nexport interface BloodPressureReading extends ReadingBase {\n kind: 'blood-pressure';\n systolic: number;\n diastolic: number;\n pulse?: number;\n /** What the monitor flagged (moved, loose cuff, irregular pulse), in words. */\n problems?: string[];\n}\n\nexport interface TemperatureReading extends ReadingBase {\n kind: 'temperature';\n celsius: number;\n}\n\nexport interface OxygenReading extends ReadingBase {\n kind: 'spo2';\n spo2: number;\n pulse?: number;\n}\n\nexport interface GlucoseReading extends ReadingBase {\n kind: 'glucose';\n mgdl: number;\n meal?: MealRelation;\n}\n\n/** A heart-rate strap worn for a session: the samples, summed up when it ended. */\nexport interface HeartRateSession extends ReadingBase {\n kind: 'heart-rate';\n /** `at` is the start. */\n end: number;\n /** run | walk | cycle | strength | hiit | other, as the person picked it. */\n workoutType: string;\n avgHr: number;\n maxHr: number;\n /** At most one sample every few seconds (see thinSamples); emptied once saved, see compactSession. */\n samples: TimedValue[];\n /** Minutes per heart-rate zone, kept when the samples are let go. */\n zoneMinutes?: number[];\n}\n\n/**\n * A finished exercise a device recorded itself: a jump-rope session or a\n * treadmill walk/run. Becomes one HealthWorkout in the day (readings with no\n * heart-rate samples, unlike HeartRateSession).\n */\nexport interface WorkoutReading extends ReadingBase {\n kind: 'workout';\n /** `at` is the start. */\n end: number;\n /** jump-rope | run | walk | ... lowercased. */\n workoutType: string;\n durationMin: number;\n calories?: number;\n distanceMeters?: number;\n /** Jump rope only: the number of skips. */\n jumps?: number;\n avgHr?: number;\n maxHr?: number;\n}\n\n/** A tape-measure girth reading of one body part. Only the waist has a home in the day. */\nexport interface BodyMeasurementReading extends ReadingBase {\n kind: 'body-measurement';\n /** waist | chest | hip | left_arm | ... as the tape reported it. */\n bodyPart: string;\n cm: number;\n}\n\nexport type DeviceReading =\n | BodyReading\n | BloodPressureReading\n | TemperatureReading\n | OxygenReading\n | GlucoseReading\n | HeartRateSession\n | WorkoutReading\n | BodyMeasurementReading;\n\nconst round1 = (n: number) => Math.round(n * 10) / 10;\n\n/** A stable id: the same measurement from the same device always gets the same one. */\nexport function readingId(deviceId: string, kind: DeviceReading['kind'], at: number, extra?: string | number): string {\n return [deviceId, kind, Math.round(at / 1000), extra].filter((p) => p !== undefined && p !== '').join(':');\n}\n\n/** Keep one heart-rate sample per `everyMs`: enough for zones, few enough to store and save. */\nexport function thinSamples(samples: readonly TimedValue[], everyMs = 5_000): TimedValue[] {\n const out: TimedValue[] = [];\n let last = -Infinity;\n for (const s of [...samples].sort((a, b) => a.t - b.t)) {\n if (s.t - last >= everyMs) {\n out.push(s);\n last = s.t;\n }\n }\n return out;\n}\n\nconst latestOf = <T extends DeviceReading>(list: readonly T[], has: (r: T) => boolean): T | undefined =>\n list.filter(has).reduce<T | undefined>((a, b) => (!a || b.at >= a.at ? b : a), undefined);\n\nexport interface DeviceDaysOptions {\n clock: ZoneClock;\n syncedAt: string;\n /** 208 - 0.7 x age, for heart-rate zones; undefined when the birth date is unknown. */\n maxHr?: number;\n /** Only these dates (the ones a new reading touched); every date when omitted. */\n dates?: ReadonlySet<string>;\n}\n\n/** Every reading -> one `device` HealthDay per local date, cleaned exactly as the backend cleans. */\nexport function deviceHealthDays(\n readings: readonly DeviceReading[],\n opts: DeviceDaysOptions,\n): { days: HealthDay[]; dropped: string[] } {\n const byDate = new Map<string, DeviceReading[]>();\n for (const r of readings) {\n const date = opts.clock.dateOf(r.at);\n if (opts.dates && !opts.dates.has(date)) continue;\n byDate.set(date, [...(byDate.get(date) ?? []), r]);\n }\n const dropped: string[] = [];\n const days: HealthDay[] = [];\n for (const [date, list] of [...byDate.entries()].sort(([a], [b]) => a.localeCompare(b))) {\n const day: HealthDay = { date, source: 'device' };\n const body = list.filter((r): r is BodyReading => r.kind === 'body');\n const weight = latestOf(body, (r) => r.weightKg !== undefined);\n if (weight) day.weightKg = weight.weightKg;\n const fat = latestOf(body, (r) => r.bodyFatPct !== undefined);\n if (fat) day.bodyFatPct = fat.bodyFatPct;\n const lean = latestOf(body, (r) => r.leanMassKg !== undefined);\n if (lean) day.leanMassKg = lean.leanMassKg;\n const bmi = latestOf(body, (r) => r.bmi !== undefined);\n if (bmi) day.bmi = bmi.bmi;\n const temp = latestOf(\n list.filter((r): r is TemperatureReading => r.kind === 'temperature'),\n () => true,\n );\n if (temp) day.bodyTempC = temp.celsius;\n const spo2 = latestOf(\n list.filter((r): r is OxygenReading => r.kind === 'spo2'),\n () => true,\n );\n if (spo2) day.spo2 = spo2.spo2;\n const waist = latestOf(\n list.filter((r): r is BodyMeasurementReading => r.kind === 'body-measurement' && r.bodyPart === 'waist'),\n () => true,\n );\n if (waist) day.waistCm = waist.cm;\n\n const readingsOut: HealthReading[] = [];\n for (const r of list) {\n if (r.kind === 'blood-pressure') {\n readingsOut.push({\n id: r.id,\n at: new Date(r.at).toISOString(),\n kind: 'blood-pressure',\n systolic: r.systolic,\n diastolic: r.diastolic,\n ...(r.pulse !== undefined ? { pulse: r.pulse } : {}),\n device: r.deviceName,\n });\n } else if (r.kind === 'glucose') {\n readingsOut.push({\n id: r.id,\n at: new Date(r.at).toISOString(),\n kind: 'glucose',\n glucoseMgDl: r.mgdl,\n ...(r.meal ? { meal: r.meal } : {}),\n device: r.deviceName,\n });\n }\n }\n if (readingsOut.length) day.readings = readingsOut.sort((a, b) => a.at.localeCompare(b.at));\n\n const workouts: HealthWorkout[] = list\n .filter((r): r is HeartRateSession => r.kind === 'heart-rate')\n .sort((a, b) => a.at - b.at)\n .map((s) => {\n const w: HealthWorkout = {\n id: s.id,\n start: new Date(s.at).toISOString(),\n end: new Date(s.end).toISOString(),\n type: s.workoutType,\n durationMin: round1((s.end - s.at) / 60_000),\n avgHr: s.avgHr,\n maxHr: s.maxHr,\n };\n if (opts.maxHr && s.samples.length) {\n w.zoneMinutes = zoneMinutes(s.samples, { start: s.at, end: s.end }, opts.maxHr);\n } else if (s.zoneMinutes) w.zoneMinutes = s.zoneMinutes;\n return w;\n });\n // Device-recorded workouts (jump rope, treadmill): same HealthWorkout shape, no HR samples.\n for (const s of list.filter((r): r is WorkoutReading => r.kind === 'workout').sort((a, b) => a.at - b.at)) {\n const w: HealthWorkout = {\n id: s.id,\n start: new Date(s.at).toISOString(),\n end: new Date(s.end).toISOString(),\n type: s.workoutType,\n durationMin: round1(s.durationMin),\n ...(s.calories !== undefined ? { calories: s.calories } : {}),\n ...(s.distanceMeters !== undefined ? { distanceMeters: s.distanceMeters } : {}),\n ...(s.avgHr !== undefined ? { avgHr: s.avgHr } : {}),\n ...(s.maxHr !== undefined ? { maxHr: s.maxHr } : {}),\n };\n workouts.push(w);\n }\n if (workouts.length) day.workouts = workouts.sort((a, b) => a.start.localeCompare(b.start));\n\n const clean = sanitizeDay(day, dropped);\n if (hasData(clean)) days.push({ ...clean, syncedAt: opts.syncedAt });\n }\n return { days, dropped };\n}\n\n/** One line for a reading, as the person reads it (\"82.4 kg, body fat 23.1%\"). */\nexport function describeReading(r: DeviceReading, units: 'metric' | 'imperial' = 'metric'): string {\n const kg = (v: number) => (units === 'imperial' ? `${round1(v / 0.45359237)} lb` : `${round1(v)} kg`);\n switch (r.kind) {\n case 'body': {\n const parts: string[] = [];\n if (r.weightKg !== undefined) parts.push(kg(r.weightKg));\n if (r.bodyFatPct !== undefined) parts.push(`body fat ${round1(r.bodyFatPct)}%`);\n if (r.bmi !== undefined) parts.push(`BMI ${round1(r.bmi)}`);\n return parts.join(', ') || 'A measurement with no weight';\n }\n case 'blood-pressure':\n return `${Math.round(r.systolic)}/${Math.round(r.diastolic)} mmHg${r.pulse ? `, pulse ${Math.round(r.pulse)}` : ''}`;\n case 'temperature':\n return units === 'imperial' ? `${round1((r.celsius * 9) / 5 + 32)} °F` : `${round1(r.celsius)} °C`;\n case 'spo2':\n return `Blood oxygen ${Math.round(r.spo2)}%${r.pulse ? `, pulse ${Math.round(r.pulse)}` : ''}`;\n case 'glucose':\n return `${Math.round(r.mgdl)} mg/dL${r.meal ? `, ${r.meal.replace('-', ' ')}` : ''}`;\n case 'heart-rate': {\n const minutes = Math.round((r.end - r.at) / 60_000);\n return `${minutes} min, average ${Math.round(r.avgHr)} bpm, max ${Math.round(r.maxHr)}`;\n }\n case 'workout': {\n const parts: string[] = [`${Math.round(r.durationMin)} min`];\n if (r.jumps !== undefined) parts.unshift(`${r.jumps} jumps`);\n if (r.distanceMeters !== undefined) parts.push(`${round1(r.distanceMeters / 1000)} km`);\n if (r.calories !== undefined) parts.push(`${Math.round(r.calories)} kcal`);\n return parts.join(', ');\n }\n case 'body-measurement': {\n const part = r.bodyPart.replace(/_/g, ' ');\n return units === 'imperial' ? `${part} ${round1(r.cm / 2.54)} in` : `${part} ${round1(r.cm)} cm`;\n }\n default:\n return '';\n }\n}\n\n/**\n * A session's samples are only needed until they are uploaded and saved to\n * the health store; after that the zones are kept and the samples let go, so\n * the phone's store stays small.\n */\nexport function compactSession(s: HeartRateSession, maxHr: number | undefined): HeartRateSession {\n if (!s.samples.length) return s;\n const zones = maxHr ? zoneMinutes(s.samples, { start: s.at, end: s.end }, maxHr) : s.zoneMinutes;\n return { ...s, samples: [], ...(zones ? { zoneMinutes: zones } : {}) };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"readings.js","sources":["../../../src/features/devices/readings.ts"],"sourcesContent":["/**\n * What a paired Bluetooth device measured, and how it reaches the Fitness\n * backend.\n *\n * The backend keeps ONE day per source and replaces it whole on every upload,\n * so a reading is never sent alone: the app keeps every reading it took (the\n * device store), and each upload rebuilds the full `device` day for the dates\n * that changed. A blood-pressure reading in the evening never wipes the\n * morning's weigh-in.\n *\n * Reading ids are stable (device + what + when): a monitor that sends its\n * whole memory on every connection, or the same reading arriving twice, is\n * stored once.\n */\n\nimport type { BodyComposition } from './bodyComposition';\nimport { sanitizeDay, hasData } from '../health/buildDays';\nimport type { TimedValue } from '../health/model';\nimport type { HealthDay, HealthReading, HealthWorkout, MealRelation } from '../health/types';\nimport type { ZoneClock } from '../health/time';\nimport { zoneMinutes } from '../health/zones';\n\ninterface ReadingBase {\n id: string;\n /** Epoch ms of the measurement. */\n at: number;\n deviceId: string;\n deviceName: string;\n}\n\nexport interface BodyReading extends ReadingBase {\n kind: 'body';\n weightKg?: number;\n bodyFatPct?: number;\n leanMassKg?: number;\n bmi?: number;\n /**\n * Whole-body impedance in ohms, when the scale sends it. A scale that only\n * reports weight and impedance (RENPHO's common protocol) leaves body fat\n * to Yantra's own estimate; one that computes body fat itself (R-A017) also\n * fills bodyFatPct and bmi.\n */\n impedanceOhm?: number;\n /**\n * The full body composition (muscle, bone, water, BMR, metabolic age ...),\n * computed from weight + body fat + the person's height/age/sex when all are\n * known (bodyComposition.ts). Shown on the reading; the backend day keeps\n * the fields it supports.\n */\n composition?: BodyComposition;\n}\n\nexport interface BloodPressureReading extends ReadingBase {\n kind: 'blood-pressure';\n systolic: number;\n diastolic: number;\n pulse?: number;\n /** What the monitor flagged (moved, loose cuff, irregular pulse), in words. */\n problems?: string[];\n}\n\nexport interface TemperatureReading extends ReadingBase {\n kind: 'temperature';\n celsius: number;\n}\n\nexport interface OxygenReading extends ReadingBase {\n kind: 'spo2';\n spo2: number;\n pulse?: number;\n}\n\nexport interface GlucoseReading extends ReadingBase {\n kind: 'glucose';\n mgdl: number;\n meal?: MealRelation;\n}\n\n/** A heart-rate strap worn for a session: the samples, summed up when it ended. */\nexport interface HeartRateSession extends ReadingBase {\n kind: 'heart-rate';\n /** `at` is the start. */\n end: number;\n /** run | walk | cycle | strength | hiit | other, as the person picked it. */\n workoutType: string;\n avgHr: number;\n maxHr: number;\n /** At most one sample every few seconds (see thinSamples); emptied once saved, see compactSession. */\n samples: TimedValue[];\n /** Minutes per heart-rate zone, kept when the samples are let go. */\n zoneMinutes?: number[];\n}\n\n/**\n * A finished exercise a device recorded itself: a jump-rope session or a\n * treadmill walk/run. Becomes one HealthWorkout in the day (readings with no\n * heart-rate samples, unlike HeartRateSession).\n */\nexport interface WorkoutReading extends ReadingBase {\n kind: 'workout';\n /** `at` is the start. */\n end: number;\n /** jump-rope | run | walk | ... lowercased. */\n workoutType: string;\n durationMin: number;\n calories?: number;\n distanceMeters?: number;\n /** Jump rope only: the number of skips. */\n jumps?: number;\n avgHr?: number;\n maxHr?: number;\n}\n\n/** A tape-measure girth reading of one body part. Only the waist has a home in the day. */\nexport interface BodyMeasurementReading extends ReadingBase {\n kind: 'body-measurement';\n /** waist | chest | hip | left_arm | ... as the tape reported it. */\n bodyPart: string;\n cm: number;\n}\n\nexport type DeviceReading =\n | BodyReading\n | BloodPressureReading\n | TemperatureReading\n | OxygenReading\n | GlucoseReading\n | HeartRateSession\n | WorkoutReading\n | BodyMeasurementReading;\n\nconst round1 = (n: number) => Math.round(n * 10) / 10;\n\n/** A stable id: the same measurement from the same device always gets the same one. */\nexport function readingId(deviceId: string, kind: DeviceReading['kind'], at: number, extra?: string | number): string {\n return [deviceId, kind, Math.round(at / 1000), extra].filter((p) => p !== undefined && p !== '').join(':');\n}\n\n/** Keep one heart-rate sample per `everyMs`: enough for zones, few enough to store and save. */\nexport function thinSamples(samples: readonly TimedValue[], everyMs = 5_000): TimedValue[] {\n const out: TimedValue[] = [];\n let last = -Infinity;\n for (const s of [...samples].sort((a, b) => a.t - b.t)) {\n if (s.t - last >= everyMs) {\n out.push(s);\n last = s.t;\n }\n }\n return out;\n}\n\nconst latestOf = <T extends DeviceReading>(list: readonly T[], has: (r: T) => boolean): T | undefined =>\n list.filter(has).reduce<T | undefined>((a, b) => (!a || b.at >= a.at ? b : a), undefined);\n\nexport interface DeviceDaysOptions {\n clock: ZoneClock;\n syncedAt: string;\n /** 208 - 0.7 x age, for heart-rate zones; undefined when the birth date is unknown. */\n maxHr?: number;\n /** Only these dates (the ones a new reading touched); every date when omitted. */\n dates?: ReadonlySet<string>;\n}\n\n/** Every reading -> one `device` HealthDay per local date, cleaned exactly as the backend cleans. */\nexport function deviceHealthDays(\n readings: readonly DeviceReading[],\n opts: DeviceDaysOptions,\n): { days: HealthDay[]; dropped: string[] } {\n const byDate = new Map<string, DeviceReading[]>();\n for (const r of readings) {\n const date = opts.clock.dateOf(r.at);\n if (opts.dates && !opts.dates.has(date)) continue;\n byDate.set(date, [...(byDate.get(date) ?? []), r]);\n }\n const dropped: string[] = [];\n const days: HealthDay[] = [];\n for (const [date, list] of [...byDate.entries()].sort(([a], [b]) => a.localeCompare(b))) {\n const day: HealthDay = { date, source: 'device' };\n const body = list.filter((r): r is BodyReading => r.kind === 'body');\n const weight = latestOf(body, (r) => r.weightKg !== undefined);\n if (weight) day.weightKg = weight.weightKg;\n const fat = latestOf(body, (r) => r.bodyFatPct !== undefined);\n if (fat) day.bodyFatPct = fat.bodyFatPct;\n const lean = latestOf(body, (r) => r.leanMassKg !== undefined);\n if (lean) day.leanMassKg = lean.leanMassKg;\n const bmi = latestOf(body, (r) => r.bmi !== undefined);\n if (bmi) day.bmi = bmi.bmi;\n const temp = latestOf(\n list.filter((r): r is TemperatureReading => r.kind === 'temperature'),\n () => true,\n );\n if (temp) day.bodyTempC = temp.celsius;\n const spo2 = latestOf(\n list.filter((r): r is OxygenReading => r.kind === 'spo2'),\n () => true,\n );\n if (spo2) day.spo2 = spo2.spo2;\n const waist = latestOf(\n list.filter((r): r is BodyMeasurementReading => r.kind === 'body-measurement' && r.bodyPart === 'waist'),\n () => true,\n );\n if (waist) day.waistCm = waist.cm;\n\n const readingsOut: HealthReading[] = [];\n for (const r of list) {\n if (r.kind === 'blood-pressure') {\n readingsOut.push({\n id: r.id,\n at: new Date(r.at).toISOString(),\n kind: 'blood-pressure',\n systolic: r.systolic,\n diastolic: r.diastolic,\n ...(r.pulse !== undefined ? { pulse: r.pulse } : {}),\n device: r.deviceName,\n });\n } else if (r.kind === 'glucose') {\n readingsOut.push({\n id: r.id,\n at: new Date(r.at).toISOString(),\n kind: 'glucose',\n glucoseMgDl: r.mgdl,\n ...(r.meal ? { meal: r.meal } : {}),\n device: r.deviceName,\n });\n }\n }\n if (readingsOut.length) day.readings = readingsOut.sort((a, b) => a.at.localeCompare(b.at));\n\n const workouts: HealthWorkout[] = list\n .filter((r): r is HeartRateSession => r.kind === 'heart-rate')\n .sort((a, b) => a.at - b.at)\n .map((s) => {\n const w: HealthWorkout = {\n id: s.id,\n start: new Date(s.at).toISOString(),\n end: new Date(s.end).toISOString(),\n type: s.workoutType,\n durationMin: round1((s.end - s.at) / 60_000),\n avgHr: s.avgHr,\n maxHr: s.maxHr,\n };\n if (opts.maxHr && s.samples.length) {\n w.zoneMinutes = zoneMinutes(s.samples, { start: s.at, end: s.end }, opts.maxHr);\n } else if (s.zoneMinutes) w.zoneMinutes = s.zoneMinutes;\n return w;\n });\n // Device-recorded workouts (jump rope, treadmill): same HealthWorkout shape, no HR samples.\n for (const s of list.filter((r): r is WorkoutReading => r.kind === 'workout').sort((a, b) => a.at - b.at)) {\n const w: HealthWorkout = {\n id: s.id,\n start: new Date(s.at).toISOString(),\n end: new Date(s.end).toISOString(),\n type: s.workoutType,\n durationMin: round1(s.durationMin),\n ...(s.calories !== undefined ? { calories: s.calories } : {}),\n ...(s.distanceMeters !== undefined ? { distanceMeters: s.distanceMeters } : {}),\n ...(s.avgHr !== undefined ? { avgHr: s.avgHr } : {}),\n ...(s.maxHr !== undefined ? { maxHr: s.maxHr } : {}),\n };\n workouts.push(w);\n }\n if (workouts.length) day.workouts = workouts.sort((a, b) => a.start.localeCompare(b.start));\n\n const clean = sanitizeDay(day, dropped);\n if (hasData(clean)) days.push({ ...clean, syncedAt: opts.syncedAt });\n }\n return { days, dropped };\n}\n\n/** One line for a reading, as the person reads it (\"82.4 kg, body fat 23.1%\"). */\nexport function describeReading(r: DeviceReading, units: 'metric' | 'imperial' = 'metric'): string {\n const kg = (v: number) => (units === 'imperial' ? `${round1(v / 0.45359237)} lb` : `${round1(v)} kg`);\n switch (r.kind) {\n case 'body': {\n const parts: string[] = [];\n if (r.weightKg !== undefined) parts.push(kg(r.weightKg));\n if (r.bodyFatPct !== undefined) parts.push(`body fat ${round1(r.bodyFatPct)}%`);\n if (r.bmi !== undefined) parts.push(`BMI ${round1(r.bmi)}`);\n return parts.join(', ') || 'A measurement with no weight';\n }\n case 'blood-pressure':\n return `${Math.round(r.systolic)}/${Math.round(r.diastolic)} mmHg${r.pulse ? `, pulse ${Math.round(r.pulse)}` : ''}`;\n case 'temperature':\n return units === 'imperial' ? `${round1((r.celsius * 9) / 5 + 32)} °F` : `${round1(r.celsius)} °C`;\n case 'spo2':\n return `Blood oxygen ${Math.round(r.spo2)}%${r.pulse ? `, pulse ${Math.round(r.pulse)}` : ''}`;\n case 'glucose':\n return `${Math.round(r.mgdl)} mg/dL${r.meal ? `, ${r.meal.replace('-', ' ')}` : ''}`;\n case 'heart-rate': {\n const minutes = Math.round((r.end - r.at) / 60_000);\n return `${minutes} min, average ${Math.round(r.avgHr)} bpm, max ${Math.round(r.maxHr)}`;\n }\n case 'workout': {\n const parts: string[] = [`${Math.round(r.durationMin)} min`];\n if (r.jumps !== undefined) parts.unshift(`${r.jumps} jumps`);\n if (r.distanceMeters !== undefined) parts.push(`${round1(r.distanceMeters / 1000)} km`);\n if (r.calories !== undefined) parts.push(`${Math.round(r.calories)} kcal`);\n return parts.join(', ');\n }\n case 'body-measurement': {\n const part = r.bodyPart.replace(/_/g, ' ');\n return units === 'imperial' ? `${part} ${round1(r.cm / 2.54)} in` : `${part} ${round1(r.cm)} cm`;\n }\n default:\n return '';\n }\n}\n\n/**\n * A session's samples are only needed until they are uploaded and saved to\n * the health store; after that the zones are kept and the samples let go, so\n * the phone's store stays small.\n */\nexport function compactSession(s: HeartRateSession, maxHr: number | undefined): HeartRateSession {\n if (!s.samples.length) return s;\n const zones = maxHr ? zoneMinutes(s.samples, { start: s.at, end: s.end }, maxHr) : s.zoneMinutes;\n return { ...s, samples: [], ...(zones ? { zoneMinutes: zones } : {}) };\n}\n"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import {deviceKind}from'./catalog.js';import {describeBleError}from'./link.js';import {thinSamples,readingId}from'./readings.js';import'./brands/frames.js';import'./brands/uuids.js';import'./gatt/uuids.js';import {detectBrand}from'./brands/detect.js';import {startDriver}from'./drivers.js';var __defProp = Object.defineProperty;
1
+ import {deviceKind}from'./catalog.js';import {describeBleError}from'./link.js';import {thinSamples,readingId}from'./readings.js';import'./brands/frames.js';import'./brands/uuids.js';import'./gatt/uuids.js';import {detectBrand}from'./brands/detect.js';import {startDriver}from'./drivers.js';import {computeBodyComposition}from'./bodyComposition.js';var __defProp = Object.defineProperty;
2
2
  var __defProps = Object.defineProperties;
3
3
  var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
4
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
@@ -24,6 +24,26 @@ function ageFrom(birthDate, now) {
24
24
  const t = Date.parse(`${birthDate}T00:00:00Z`);
25
25
  return Number.isFinite(t) ? Math.floor((now - t) / (365.25 * 864e5)) : void 0;
26
26
  }
27
+ function enrichWithComposition(reading, facts) {
28
+ var _a, _b, _c;
29
+ if (reading.kind !== "body" || reading.weightKg === void 0 || reading.bodyFatPct === void 0) {
30
+ return reading;
31
+ }
32
+ if (!facts.heightCm || facts.age === void 0) return reading;
33
+ const composition = computeBodyComposition({
34
+ weightKg: reading.weightKg,
35
+ bodyFatPct: reading.bodyFatPct,
36
+ heightCm: facts.heightCm,
37
+ age: facts.age,
38
+ sex: (_a = facts.sex) != null ? _a : "unspecified",
39
+ impedanceOhm: reading.impedanceOhm
40
+ });
41
+ return __spreadProps(__spreadValues({}, reading), {
42
+ bmi: (_b = reading.bmi) != null ? _b : composition.bmi,
43
+ leanMassKg: (_c = reading.leanMassKg) != null ? _c : composition.leanMassKg,
44
+ composition
45
+ });
46
+ }
27
47
  class DeviceSession {
28
48
  constructor(deps) {
29
49
  this.deps = deps;
@@ -187,7 +207,7 @@ class DeviceSession {
187
207
  } : void 0,
188
208
  scaleUser: paired == null ? void 0 : paired.scaleUser,
189
209
  saveScaleUser: (user) => store.saveScaleUser(link.id, user),
190
- onReading: (r) => void this.keep([r]),
210
+ onReading: (r) => void this.keep([enrichWithComposition(r, facts)]),
191
211
  onLive: (v) => this.onLive(v),
192
212
  onNotice: (m) => this.set({
193
213
  notices: [...this.snapshot.notices, m]
@@ -1 +1 @@
1
- {"version":3,"file":"session.js","sources":["../../../src/features/devices/session.ts"],"sourcesContent":["/**\n * One \"take a reading\" from start to end, for the screen: is Bluetooth on and\n * allowed, find the device (or the one already paired), connect, run its\n * driver, keep what it measures, and end cleanly when it goes to sleep (a\n * scale switches off seconds after a weighing; that is the normal end, not an\n * error).\n *\n * Heart-rate straps and continuous oximeters stream: the person starts and\n * stops a session (heart rate) or saves the value they see (oxygen).\n *\n * Plain class with injected transport and store, like the other stores: the\n * screen subscribes with useSyncExternalStore, tests drive it with fakes.\n */\nimport type { TimedValue } from '../health/model';\nimport { deviceKind, type DeviceKindId } from './catalog';\nimport { describeBleError, type BleTransport, type ConnectedLink, type FoundDevice, type RadioState } from './link';\nimport { detectBrand } from './brands';\nimport type { DeviceStore } from './deviceStore';\nimport { startDriver, type DriverHandle, type LiveValue, type PersonFacts } from './drivers';\nimport { readingId, thinSamples, type DeviceReading, type HeartRateSession } from './readings';\n\nexport type SessionState =\n | { step: 'starting' }\n | { step: 'unavailable'; reason: string }\n | { step: 'radio'; radio: Exclude<RadioState, 'ready'> }\n | { step: 'scanning'; found: FoundDevice[]; hint?: string }\n | { step: 'connecting'; device: FoundDevice }\n | { step: 'connected'; device: { id: string; name: string } }\n | { step: 'ended'; device: { id: string; name: string } }\n | { step: 'error'; message: string };\n\nexport interface SessionSnapshot {\n state: SessionState;\n kind: DeviceKindId;\n /** What this session measured, newest first. */\n readings: DeviceReading[];\n live?: LiveValue;\n recording?: { startedAt: number; workoutType: string; avgHr?: number; maxHr?: number };\n notices: string[];\n}\n\nexport interface SessionDeps {\n transport: BleTransport | null;\n store: Pick<DeviceStore, 'pair' | 'addReadings' | 'saveScaleUser' | 'person' | 'getSnapshot'>;\n kind: DeviceKindId;\n /** Reconnect to this paired device instead of offering a list. */\n deviceId?: string;\n now?: () => number;\n}\n\n/** Scanning this long without finding anything: tell the person how to wake it. */\nexport const SCAN_HINT_MS = 20_000;\n\nfunction ageFrom(birthDate: string | undefined, now: number): number | undefined {\n if (!birthDate) return undefined;\n const t = Date.parse(`${birthDate}T00:00:00Z`);\n return Number.isFinite(t) ? Math.floor((now - t) / (365.25 * 86_400_000)) : undefined;\n}\n\nexport class DeviceSession {\n private snapshot: SessionSnapshot;\n\n private listeners = new Set<() => void>();\n\n private stopScan: (() => void) | null = null;\n\n private link: ConnectedLink | null = null;\n\n private driver: DriverHandle | null = null;\n\n private hintTimer: ReturnType<typeof setTimeout> | null = null;\n\n private hr: { samples: TimedValue[]; sum: number; count: number; max: number } | null = null;\n\n private closed = false;\n\n constructor(private readonly deps: SessionDeps) {\n this.snapshot = { state: { step: 'starting' }, kind: deps.kind, readings: [], notices: [] };\n }\n\n subscribe = (listener: () => void) => {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n };\n\n getSnapshot = (): SessionSnapshot => this.snapshot;\n\n private now(): number {\n return this.deps.now ? this.deps.now() : Date.now();\n }\n\n private set(patch: Partial<SessionSnapshot>) {\n if (this.closed) return;\n this.snapshot = { ...this.snapshot, ...patch };\n this.listeners.forEach((l) => l());\n }\n\n async start(): Promise<void> {\n const { transport } = this.deps;\n if (!transport) {\n this.set({\n state: { step: 'unavailable', reason: 'Bluetooth devices need the latest version of the Yantra app.' },\n });\n return;\n }\n if (!(await transport.requestPermissions())) {\n this.set({ state: { step: 'radio', radio: 'unauthorized' } });\n return;\n }\n const radio = await transport.radio();\n if (radio !== 'ready' && radio !== 'unknown') {\n this.set({ state: { step: 'radio', radio } });\n return;\n }\n this.scan();\n }\n\n /** Back to looking for the device (after the radio came on, or to measure again). */\n scan(): void {\n const { transport } = this.deps;\n if (!transport || this.closed) return;\n this.stopScan?.();\n this.set({ state: { step: 'scanning', found: [] } });\n this.hintTimer = setTimeout(() => {\n const s = this.snapshot.state;\n if (s.step === 'scanning' && !s.found.length) {\n this.set({ state: { ...s, hint: deviceKind(this.deps.kind).steps[0] } });\n }\n }, SCAN_HINT_MS);\n const kind = this.deps.kind;\n this.stopScan = transport.scan(\n {\n services: deviceKind(kind).services,\n // Brand devices do not advertise a standard service: recognise this kind of them.\n match: (adv) => {\n const brand = detectBrand(adv);\n return brand && brand.kind === kind ? brand : undefined;\n },\n },\n (found) => {\n const s = this.snapshot.state;\n if (s.step !== 'scanning') return;\n if (this.deps.deviceId && found.id === this.deps.deviceId) {\n void this.pick(found);\n return;\n }\n if (this.deps.deviceId) return;\n this.set({\n state: {\n ...s,\n hint: undefined,\n found: [...s.found, found].sort((a, b) => (b.rssi ?? -999) - (a.rssi ?? -999)),\n },\n });\n },\n (message) => this.set({ state: { step: 'error', message } }),\n );\n }\n\n async pick(found: FoundDevice): Promise<void> {\n const { transport, store } = this.deps;\n if (!transport) return;\n this.clearScan();\n this.set({ state: { step: 'connecting', device: found } });\n try {\n const link = await transport.connect(found.id, found.name, () => this.onDisconnect());\n this.link = link;\n const paired = store.getSnapshot().devices.find((d) => d.id === link.id);\n const person = await store.person();\n const facts: PersonFacts = {\n age: ageFrom(person?.birthDate, this.now()),\n sex: person?.sex,\n heightCm: person?.heightCm,\n birthDate: person?.birthDate,\n weightKg: paired?.lastWeightKg,\n };\n this.driver = await startDriver(this.deps.kind, link, {\n now: () => this.now(),\n person: facts,\n identity: found.protocol\n ? {\n protocol: found.protocol,\n mac: found.mac,\n deviceType: found.deviceType,\n model: found.model,\n kind: found.kind,\n }\n : undefined,\n scaleUser: paired?.scaleUser,\n saveScaleUser: (user) => store.saveScaleUser(link.id, user),\n onReading: (r) => void this.keep([r]),\n onLive: (v) => this.onLive(v),\n onNotice: (m) => this.set({ notices: [...this.snapshot.notices, m] }),\n });\n await store.pair({\n id: link.id,\n name: link.name,\n kind: this.deps.kind,\n ...(found.protocol ? { protocol: found.protocol } : {}),\n });\n this.set({ state: { step: 'connected', device: { id: link.id, name: link.name } } });\n } catch (err) {\n await this.link?.close();\n this.link = null;\n this.set({ state: { step: 'error', message: describeErrorOrOwn(err) } });\n }\n }\n\n private async keep(readings: DeviceReading[]): Promise<void> {\n const fresh = await this.deps.store.addReadings(readings);\n if (fresh.length) this.set({ readings: [...fresh.reverse(), ...this.snapshot.readings] });\n }\n\n private onLive(v: LiveValue) {\n this.set({ live: { ...this.snapshot.live, ...v } });\n if (this.hr && v.bpm) {\n const t = this.now();\n this.hr.samples.push({ t, value: v.bpm });\n this.hr.sum += v.bpm;\n this.hr.count += 1;\n this.hr.max = Math.max(this.hr.max, v.bpm);\n const rec = this.snapshot.recording;\n if (rec)\n this.set({ recording: { ...rec, avgHr: Math.round(this.hr.sum / this.hr.count), maxHr: this.hr.max } });\n }\n }\n\n /** Heart rate: start recording a session of the given kind (run, walk, cycle, strength ...). */\n startRecording(workoutType: string): void {\n this.hr = { samples: [], sum: 0, count: 0, max: 0 };\n this.set({ recording: { startedAt: this.now(), workoutType } });\n }\n\n /** Heart rate: end the session and keep it. Nothing is kept for a session with no heart rate. */\n async stopRecording(): Promise<HeartRateSession | null> {\n const rec = this.snapshot.recording;\n const hr = this.hr;\n this.hr = null;\n this.set({ recording: undefined });\n const device = this.link;\n if (!rec || !hr || !hr.count || !device) return null;\n const end = this.now();\n const session: HeartRateSession = {\n kind: 'heart-rate',\n id: readingId(device.id, 'heart-rate', rec.startedAt),\n at: rec.startedAt,\n end,\n deviceId: device.id,\n deviceName: device.name,\n workoutType: rec.workoutType,\n avgHr: Math.round(hr.sum / hr.count),\n maxHr: hr.max,\n samples: thinSamples(hr.samples),\n };\n await this.keep([session]);\n return session;\n }\n\n /** Continuous oximeter: keep the value on screen as a reading. */\n async saveLive(): Promise<void> {\n const live = this.snapshot.live;\n const device = this.link;\n if (!device || live?.spo2 === undefined) return;\n const at = this.now();\n await this.keep([\n {\n kind: 'spo2',\n id: readingId(device.id, 'spo2', at, live.spo2),\n at,\n deviceId: device.id,\n deviceName: device.name,\n spo2: live.spo2,\n ...(live.pulse !== undefined ? { pulse: live.pulse } : {}),\n },\n ]);\n }\n\n private onDisconnect() {\n this.driver?.stop();\n this.driver = null;\n const link = this.link;\n this.link = null;\n if (this.hr) void this.stopRecording();\n const s = this.snapshot.state;\n if (s.step === 'connected' && link) {\n // A scale or cuff switches off after measuring: the normal end.\n this.set({ state: { step: 'ended', device: { id: link.id, name: link.name } }, live: undefined });\n }\n }\n\n private clearScan() {\n this.stopScan?.();\n this.stopScan = null;\n if (this.hintTimer) clearTimeout(this.hintTimer);\n this.hintTimer = null;\n }\n\n /** Leave: finish what was being assembled, stop scanning, disconnect. */\n async close(): Promise<void> {\n if (this.hr) await this.stopRecording();\n this.clearScan();\n this.driver?.stop();\n this.driver = null;\n const link = this.link;\n this.link = null;\n this.closed = true;\n await link?.close();\n }\n}\n\nfunction describeErrorOrOwn(err: unknown): string {\n // The drivers' own messages are already in plain words (\"does not send weight the standard way\").\n if (err instanceof Error && /standard way|does not know how/.test(err.message)) return err.message;\n return describeBleError(err);\n}\n"],"names":["_a"],"mappings":"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"}
1
+ {"version":3,"file":"session.js","sources":["../../../src/features/devices/session.ts"],"sourcesContent":["/**\n * One \"take a reading\" from start to end, for the screen: is Bluetooth on and\n * allowed, find the device (or the one already paired), connect, run its\n * driver, keep what it measures, and end cleanly when it goes to sleep (a\n * scale switches off seconds after a weighing; that is the normal end, not an\n * error).\n *\n * Heart-rate straps and continuous oximeters stream: the person starts and\n * stops a session (heart rate) or saves the value they see (oxygen).\n *\n * Plain class with injected transport and store, like the other stores: the\n * screen subscribes with useSyncExternalStore, tests drive it with fakes.\n */\nimport type { TimedValue } from '../health/model';\nimport { deviceKind, type DeviceKindId } from './catalog';\nimport { describeBleError, type BleTransport, type ConnectedLink, type FoundDevice, type RadioState } from './link';\nimport { detectBrand } from './brands';\nimport type { DeviceStore } from './deviceStore';\nimport { startDriver, type DriverHandle, type LiveValue, type PersonFacts } from './drivers';\nimport { computeBodyComposition } from './bodyComposition';\nimport { readingId, thinSamples, type DeviceReading, type HeartRateSession } from './readings';\n\nexport type SessionState =\n | { step: 'starting' }\n | { step: 'unavailable'; reason: string }\n | { step: 'radio'; radio: Exclude<RadioState, 'ready'> }\n | { step: 'scanning'; found: FoundDevice[]; hint?: string }\n | { step: 'connecting'; device: FoundDevice }\n | { step: 'connected'; device: { id: string; name: string } }\n | { step: 'ended'; device: { id: string; name: string } }\n | { step: 'error'; message: string };\n\nexport interface SessionSnapshot {\n state: SessionState;\n kind: DeviceKindId;\n /** What this session measured, newest first. */\n readings: DeviceReading[];\n live?: LiveValue;\n recording?: { startedAt: number; workoutType: string; avgHr?: number; maxHr?: number };\n notices: string[];\n}\n\nexport interface SessionDeps {\n transport: BleTransport | null;\n store: Pick<DeviceStore, 'pair' | 'addReadings' | 'saveScaleUser' | 'person' | 'getSnapshot'>;\n kind: DeviceKindId;\n /** Reconnect to this paired device instead of offering a list. */\n deviceId?: string;\n now?: () => number;\n}\n\n/** Scanning this long without finding anything: tell the person how to wake it. */\nexport const SCAN_HINT_MS = 20_000;\n\nfunction ageFrom(birthDate: string | undefined, now: number): number | undefined {\n if (!birthDate) return undefined;\n const t = Date.parse(`${birthDate}T00:00:00Z`);\n return Number.isFinite(t) ? Math.floor((now - t) / (365.25 * 86_400_000)) : undefined;\n}\n\n/**\n * Fill in the full body composition for a body reading once the scale has given\n * weight and body fat and the person's height/age/sex are known. Other readings\n * pass through unchanged.\n */\nfunction enrichWithComposition(reading: DeviceReading, facts: PersonFacts): DeviceReading {\n if (reading.kind !== 'body' || reading.weightKg === undefined || reading.bodyFatPct === undefined) {\n return reading;\n }\n if (!facts.heightCm || facts.age === undefined) return reading;\n const composition = computeBodyComposition({\n weightKg: reading.weightKg,\n bodyFatPct: reading.bodyFatPct,\n heightCm: facts.heightCm,\n age: facts.age,\n sex: facts.sex ?? 'unspecified',\n impedanceOhm: reading.impedanceOhm,\n });\n return {\n ...reading,\n bmi: reading.bmi ?? composition.bmi,\n leanMassKg: reading.leanMassKg ?? composition.leanMassKg,\n composition,\n };\n}\n\nexport class DeviceSession {\n private snapshot: SessionSnapshot;\n\n private listeners = new Set<() => void>();\n\n private stopScan: (() => void) | null = null;\n\n private link: ConnectedLink | null = null;\n\n private driver: DriverHandle | null = null;\n\n private hintTimer: ReturnType<typeof setTimeout> | null = null;\n\n private hr: { samples: TimedValue[]; sum: number; count: number; max: number } | null = null;\n\n private closed = false;\n\n constructor(private readonly deps: SessionDeps) {\n this.snapshot = { state: { step: 'starting' }, kind: deps.kind, readings: [], notices: [] };\n }\n\n subscribe = (listener: () => void) => {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n };\n\n getSnapshot = (): SessionSnapshot => this.snapshot;\n\n private now(): number {\n return this.deps.now ? this.deps.now() : Date.now();\n }\n\n private set(patch: Partial<SessionSnapshot>) {\n if (this.closed) return;\n this.snapshot = { ...this.snapshot, ...patch };\n this.listeners.forEach((l) => l());\n }\n\n async start(): Promise<void> {\n const { transport } = this.deps;\n if (!transport) {\n this.set({\n state: { step: 'unavailable', reason: 'Bluetooth devices need the latest version of the Yantra app.' },\n });\n return;\n }\n if (!(await transport.requestPermissions())) {\n this.set({ state: { step: 'radio', radio: 'unauthorized' } });\n return;\n }\n const radio = await transport.radio();\n if (radio !== 'ready' && radio !== 'unknown') {\n this.set({ state: { step: 'radio', radio } });\n return;\n }\n this.scan();\n }\n\n /** Back to looking for the device (after the radio came on, or to measure again). */\n scan(): void {\n const { transport } = this.deps;\n if (!transport || this.closed) return;\n this.stopScan?.();\n this.set({ state: { step: 'scanning', found: [] } });\n this.hintTimer = setTimeout(() => {\n const s = this.snapshot.state;\n if (s.step === 'scanning' && !s.found.length) {\n this.set({ state: { ...s, hint: deviceKind(this.deps.kind).steps[0] } });\n }\n }, SCAN_HINT_MS);\n const kind = this.deps.kind;\n this.stopScan = transport.scan(\n {\n services: deviceKind(kind).services,\n // Brand devices do not advertise a standard service: recognise this kind of them.\n match: (adv) => {\n const brand = detectBrand(adv);\n return brand && brand.kind === kind ? brand : undefined;\n },\n },\n (found) => {\n const s = this.snapshot.state;\n if (s.step !== 'scanning') return;\n if (this.deps.deviceId && found.id === this.deps.deviceId) {\n void this.pick(found);\n return;\n }\n if (this.deps.deviceId) return;\n this.set({\n state: {\n ...s,\n hint: undefined,\n found: [...s.found, found].sort((a, b) => (b.rssi ?? -999) - (a.rssi ?? -999)),\n },\n });\n },\n (message) => this.set({ state: { step: 'error', message } }),\n );\n }\n\n async pick(found: FoundDevice): Promise<void> {\n const { transport, store } = this.deps;\n if (!transport) return;\n this.clearScan();\n this.set({ state: { step: 'connecting', device: found } });\n try {\n const link = await transport.connect(found.id, found.name, () => this.onDisconnect());\n this.link = link;\n const paired = store.getSnapshot().devices.find((d) => d.id === link.id);\n const person = await store.person();\n const facts: PersonFacts = {\n age: ageFrom(person?.birthDate, this.now()),\n sex: person?.sex,\n heightCm: person?.heightCm,\n birthDate: person?.birthDate,\n weightKg: paired?.lastWeightKg,\n };\n this.driver = await startDriver(this.deps.kind, link, {\n now: () => this.now(),\n person: facts,\n identity: found.protocol\n ? {\n protocol: found.protocol,\n mac: found.mac,\n deviceType: found.deviceType,\n model: found.model,\n kind: found.kind,\n }\n : undefined,\n scaleUser: paired?.scaleUser,\n saveScaleUser: (user) => store.saveScaleUser(link.id, user),\n onReading: (r) => void this.keep([enrichWithComposition(r, facts)]),\n onLive: (v) => this.onLive(v),\n onNotice: (m) => this.set({ notices: [...this.snapshot.notices, m] }),\n });\n await store.pair({\n id: link.id,\n name: link.name,\n kind: this.deps.kind,\n ...(found.protocol ? { protocol: found.protocol } : {}),\n });\n this.set({ state: { step: 'connected', device: { id: link.id, name: link.name } } });\n } catch (err) {\n await this.link?.close();\n this.link = null;\n this.set({ state: { step: 'error', message: describeErrorOrOwn(err) } });\n }\n }\n\n private async keep(readings: DeviceReading[]): Promise<void> {\n const fresh = await this.deps.store.addReadings(readings);\n if (fresh.length) this.set({ readings: [...fresh.reverse(), ...this.snapshot.readings] });\n }\n\n private onLive(v: LiveValue) {\n this.set({ live: { ...this.snapshot.live, ...v } });\n if (this.hr && v.bpm) {\n const t = this.now();\n this.hr.samples.push({ t, value: v.bpm });\n this.hr.sum += v.bpm;\n this.hr.count += 1;\n this.hr.max = Math.max(this.hr.max, v.bpm);\n const rec = this.snapshot.recording;\n if (rec)\n this.set({ recording: { ...rec, avgHr: Math.round(this.hr.sum / this.hr.count), maxHr: this.hr.max } });\n }\n }\n\n /** Heart rate: start recording a session of the given kind (run, walk, cycle, strength ...). */\n startRecording(workoutType: string): void {\n this.hr = { samples: [], sum: 0, count: 0, max: 0 };\n this.set({ recording: { startedAt: this.now(), workoutType } });\n }\n\n /** Heart rate: end the session and keep it. Nothing is kept for a session with no heart rate. */\n async stopRecording(): Promise<HeartRateSession | null> {\n const rec = this.snapshot.recording;\n const hr = this.hr;\n this.hr = null;\n this.set({ recording: undefined });\n const device = this.link;\n if (!rec || !hr || !hr.count || !device) return null;\n const end = this.now();\n const session: HeartRateSession = {\n kind: 'heart-rate',\n id: readingId(device.id, 'heart-rate', rec.startedAt),\n at: rec.startedAt,\n end,\n deviceId: device.id,\n deviceName: device.name,\n workoutType: rec.workoutType,\n avgHr: Math.round(hr.sum / hr.count),\n maxHr: hr.max,\n samples: thinSamples(hr.samples),\n };\n await this.keep([session]);\n return session;\n }\n\n /** Continuous oximeter: keep the value on screen as a reading. */\n async saveLive(): Promise<void> {\n const live = this.snapshot.live;\n const device = this.link;\n if (!device || live?.spo2 === undefined) return;\n const at = this.now();\n await this.keep([\n {\n kind: 'spo2',\n id: readingId(device.id, 'spo2', at, live.spo2),\n at,\n deviceId: device.id,\n deviceName: device.name,\n spo2: live.spo2,\n ...(live.pulse !== undefined ? { pulse: live.pulse } : {}),\n },\n ]);\n }\n\n private onDisconnect() {\n this.driver?.stop();\n this.driver = null;\n const link = this.link;\n this.link = null;\n if (this.hr) void this.stopRecording();\n const s = this.snapshot.state;\n if (s.step === 'connected' && link) {\n // A scale or cuff switches off after measuring: the normal end.\n this.set({ state: { step: 'ended', device: { id: link.id, name: link.name } }, live: undefined });\n }\n }\n\n private clearScan() {\n this.stopScan?.();\n this.stopScan = null;\n if (this.hintTimer) clearTimeout(this.hintTimer);\n this.hintTimer = null;\n }\n\n /** Leave: finish what was being assembled, stop scanning, disconnect. */\n async close(): Promise<void> {\n if (this.hr) await this.stopRecording();\n this.clearScan();\n this.driver?.stop();\n this.driver = null;\n const link = this.link;\n this.link = null;\n this.closed = true;\n await link?.close();\n }\n}\n\nfunction describeErrorOrOwn(err: unknown): string {\n // The drivers' own messages are already in plain words (\"does not send weight the standard way\").\n if (err instanceof Error && /standard way|does not know how/.test(err.message)) return err.message;\n return describeBleError(err);\n}\n"],"names":["_a"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import {jsx,jsxs}from'react/jsx-runtime';import {useMemo,useRef,useEffect,useCallback,useState}from'react';import {ActivityIndicator,View,StyleSheet,ScrollView,Text,Pressable}from'react-native';import {Image}from'expo-image';import {Ionicons}from'@expo/vector-icons';import {useNavigation}from'@react-navigation/native';import {AssistantMarkdown}from'../chat/AssistantMarkdown.js';import {useChatLinkOpener}from'../links/chatLinkOpener.js';import {normalizeSources,cleanArticleContent,formatPublishedAt,faviconUrl,getStoryId}from'./articleContent.js';import {discoverPalette,openDiscoverArticle,TagChip,DISCOVER_SERIF,CompactStoryCard}from'./DiscoverStoryCards.js';import {useNewsArticle,useNewsArticlesInfinite}from'./useDiscoverNews.js';const MAX_SOURCE_CARDS = 8;
1
+ import {jsx,jsxs}from'react/jsx-runtime';import {useMemo,useRef,useState,useEffect,useCallback}from'react';import {ScrollView,StyleSheet,View,Text,Pressable,Platform}from'react-native';import {Image}from'expo-image';import {Ionicons}from'@expo/vector-icons';import {useNavigation}from'@react-navigation/native';import {AppKeyboardAvoidingView}from'../../components/AppKeyboardAvoidingView.js';import {AssistantMarkdown}from'../chat/AssistantMarkdown.js';import {useChatLinkOpener}from'../links/chatLinkOpener.js';import {normalizeSources,cleanArticleContent,formatPublishedAt,faviconUrl,getStoryId}from'./articleContent.js';import {FOLLOW_UP_RESERVED_HEIGHT,DiscoverFollowUp}from'./DiscoverFollowUp.js';import {DiscoverArticleSkeleton}from'./DiscoverSkeletons.js';import {discoverPalette,openDiscoverArticle,TagChip,DiscoverCard,DISCOVER_SERIF,DiscoverMoreCard}from'./DiscoverStoryCards.js';import {useNewsArticle,useNewsArticlesInfinite}from'./useDiscoverNews.js';const MAX_SOURCE_CARDS = 8;
2
2
  const DISCOVER_MORE_COUNT = 4;
3
3
  function SourceFavicon({
4
4
  source,
@@ -32,12 +32,7 @@ function SourceCards({
32
32
  onOpen
33
33
  }) {
34
34
  if (sources.length === 0) return null;
35
- return /* @__PURE__ */ jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, style: styles.sourcesScroll, contentContainerStyle: styles.sourcesContent, children: sources.slice(0, MAX_SOURCE_CARDS).map((source, index) => /* @__PURE__ */ jsxs(Pressable, { accessibilityRole: source.url ? "link" : void 0, disabled: !source.url, onPress: () => source.url && onOpen(source.url), style: ({
36
- pressed
37
- }) => [styles.sourceCard, {
38
- backgroundColor: palette.imagePlaceholder,
39
- borderColor: palette.cardBorder
40
- }, pressed && styles.pressed], children: [
35
+ return /* @__PURE__ */ jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, style: styles.sourcesScroll, contentContainerStyle: styles.sourcesContent, children: sources.slice(0, MAX_SOURCE_CARDS).map((source, index) => /* @__PURE__ */ jsxs(DiscoverCard, { palette, radius: 16, onPress: source.url ? () => source.url && onOpen(source.url) : void 0, accessibilityLabel: [source.name, source.title].filter(Boolean).join(", "), innerStyle: styles.sourceCard, children: [
41
36
  /* @__PURE__ */ jsxs(View, { style: styles.sourceHead, children: [
42
37
  /* @__PURE__ */ jsx(SourceFavicon, { source, size: 16, palette }),
43
38
  /* @__PURE__ */ jsx(Text, { style: [styles.sourceName, {
@@ -62,16 +57,21 @@ function DiscoverMore({
62
57
  const currentId = getStoryId(article);
63
58
  const related = articles.filter((story) => getStoryId(story) !== currentId).slice(0, DISCOVER_MORE_COUNT);
64
59
  if (related.length === 0) return null;
60
+ const rows = [];
61
+ for (let index = 0; index < related.length; index += 2) rows.push(related.slice(index, index + 2));
65
62
  return /* @__PURE__ */ jsxs(View, { style: [styles.more, {
66
63
  borderTopColor: palette.border
67
64
  }], children: [
68
65
  /* @__PURE__ */ jsxs(View, { style: styles.moreHead, children: [
69
- /* @__PURE__ */ jsx(Ionicons, { name: "sparkles-outline", size: 18, color: palette.text }),
66
+ /* @__PURE__ */ jsx(Ionicons, { name: "sparkles-outline", size: 20, color: palette.primary }),
70
67
  /* @__PURE__ */ jsx(Text, { style: [styles.moreTitle, {
71
68
  color: palette.text
72
- }], children: "Discover more" })
69
+ }], accessibilityRole: "header", children: "Discover more" })
73
70
  ] }),
74
- /* @__PURE__ */ jsx(View, { style: styles.moreList, children: related.map((story) => /* @__PURE__ */ jsx(CompactStoryCard, { story, palette, onPress: onOpen }, getStoryId(story))) })
71
+ /* @__PURE__ */ jsx(View, { style: styles.moreList, children: rows.map((row) => /* @__PURE__ */ jsxs(View, { style: styles.moreRow, children: [
72
+ row.map((story) => /* @__PURE__ */ jsx(DiscoverMoreCard, { story, palette, onPress: onOpen }, getStoryId(story))),
73
+ row.length === 1 ? /* @__PURE__ */ jsx(View, { style: styles.moreSpacer }) : null
74
+ ] }, getStoryId(row[0]))) })
75
75
  ] });
76
76
  }
77
77
  function StateMessage({
@@ -97,6 +97,8 @@ function DiscoverArticle({
97
97
  const palette = useMemo(() => discoverPalette(isDark), [isDark]);
98
98
  const openLink = useChatLinkOpener();
99
99
  const scrollRef = useRef(null);
100
+ const rootRef = useRef(null);
101
+ const [topOffset, setTopOffset] = useState(0);
100
102
  const {
101
103
  article,
102
104
  loading,
@@ -118,79 +120,86 @@ function DiscoverArticle({
118
120
  return article ? cleanArticleContent(article.content, article.title, (_a = article.summary) != null ? _a : void 0, (_b = article.imageUrl) != null ? _b : void 0) : "";
119
121
  }, [article]);
120
122
  if (!article) {
121
- let body;
122
123
  if (loading) {
123
- body = /* @__PURE__ */ jsx(ActivityIndicator, { color: palette.primary });
124
- } else if (error) {
125
- body = /* @__PURE__ */ jsx(StateMessage, { title: "Unable to load article", description: "Something went wrong while fetching this article. Please try again in a moment.", palette });
126
- } else {
127
- body = /* @__PURE__ */ jsx(StateMessage, { title: "Article not found", description: "We could not find this news article. It may not be available yet or may have been removed.", palette });
124
+ return /* @__PURE__ */ jsx(ScrollView, { style: {
125
+ backgroundColor: palette.bg
126
+ }, contentContainerStyle: styles.content, children: /* @__PURE__ */ jsx(DiscoverArticleSkeleton, { palette }) });
128
127
  }
129
128
  return /* @__PURE__ */ jsx(View, { style: [styles.state, {
130
129
  backgroundColor: palette.bg
131
- }], children: body });
130
+ }], children: error ? /* @__PURE__ */ jsx(StateMessage, { title: "Unable to load article", description: "Something went wrong while fetching this article. Please try again in a moment.", palette }) : /* @__PURE__ */ jsx(StateMessage, { title: "Article not found", description: "We could not find this news article. It may not be available yet or may have been removed.", palette }) });
132
131
  }
133
132
  const publishedLabel = formatPublishedAt(article.publishedAt);
134
133
  const sourceCountLabel = article.sourceCount === 1 ? "1 source" : `${article.sourceCount || sources.length} sources`;
135
134
  const [leadSource] = sources;
136
- return /* @__PURE__ */ jsxs(ScrollView, { ref: scrollRef, style: {
135
+ return /* @__PURE__ */ jsx(AppKeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : void 0, keyboardVerticalOffset: topOffset, style: [styles.root, {
137
136
  backgroundColor: palette.bg
138
- }, contentContainerStyle: styles.content, children: [
139
- /* @__PURE__ */ jsx(View, { style: styles.tagRow, children: /* @__PURE__ */ jsx(TagChip, { label: article.tag, palette }) }),
140
- /* @__PURE__ */ jsx(Text, { style: [styles.title, {
141
- color: palette.text
142
- }], accessibilityRole: "header", children: article.title }),
143
- article.summary ? /* @__PURE__ */ jsx(Text, { style: [styles.summary, {
144
- color: palette.text
145
- }], children: article.summary }) : null,
146
- leadSource ? /* @__PURE__ */ jsxs(Pressable, { accessibilityRole: leadSource.url ? "link" : void 0, disabled: !leadSource.url, onPress: () => leadSource.url && openLink(leadSource.url), style: [styles.leadPill, {
147
- backgroundColor: palette.imagePlaceholder,
148
- borderColor: palette.cardBorder
149
- }], children: [
150
- /* @__PURE__ */ jsx(SourceFavicon, { source: leadSource, size: 14, palette }),
151
- /* @__PURE__ */ jsx(Text, { style: [styles.leadPillText, {
152
- color: palette.textMuted
153
- }], numberOfLines: 1, children: leadSource.name }),
154
- sources.length > 1 ? /* @__PURE__ */ jsxs(Text, { style: [styles.leadPillText, {
155
- color: palette.textMuted
137
+ }], children: /* @__PURE__ */ jsxs(View, { ref: rootRef, style: styles.root, onLayout: () => {
138
+ var _a;
139
+ return (_a = rootRef.current) == null ? void 0 : _a.measureInWindow((_x, y) => setTopOffset(Math.max(0, y)));
140
+ }, children: [
141
+ /* @__PURE__ */ jsxs(ScrollView, { ref: scrollRef, style: {
142
+ backgroundColor: palette.bg
143
+ }, contentContainerStyle: [styles.content, {
144
+ paddingBottom: FOLLOW_UP_RESERVED_HEIGHT + 32
145
+ }], keyboardShouldPersistTaps: "handled", keyboardDismissMode: "interactive", children: [
146
+ /* @__PURE__ */ jsx(View, { style: styles.tagRow, children: /* @__PURE__ */ jsx(TagChip, { label: article.tag, palette }) }),
147
+ /* @__PURE__ */ jsx(Text, { style: [styles.title, {
148
+ color: palette.text
149
+ }], accessibilityRole: "header", children: article.title }),
150
+ article.summary ? /* @__PURE__ */ jsx(Text, { style: [styles.summary, {
151
+ color: palette.text
152
+ }], children: article.summary }) : null,
153
+ leadSource ? /* @__PURE__ */ jsxs(Pressable, { accessibilityRole: leadSource.url ? "link" : void 0, disabled: !leadSource.url, onPress: () => leadSource.url && openLink(leadSource.url), style: [styles.leadPill, {
154
+ backgroundColor: palette.imagePlaceholder,
155
+ borderColor: palette.cardBorder
156
156
  }], children: [
157
- "+",
158
- sources.length - 1
159
- ] }) : null
160
- ] }) : null,
161
- /* @__PURE__ */ jsxs(View, { style: [styles.byline, {
162
- borderBottomColor: palette.border
163
- }], children: [
164
- /* @__PURE__ */ jsxs(View, { style: styles.bylineItem, children: [
165
- /* @__PURE__ */ jsx(View, { style: [styles.bylineIcon, {
166
- backgroundColor: palette.text
167
- }], children: /* @__PURE__ */ jsx(Ionicons, { name: "layers-outline", size: 13, color: palette.bg }) }),
168
- /* @__PURE__ */ jsxs(Text, { style: [styles.bylineStrong, {
157
+ /* @__PURE__ */ jsx(SourceFavicon, { source: leadSource, size: 14, palette }),
158
+ /* @__PURE__ */ jsx(Text, { style: [styles.leadPillText, {
159
+ color: palette.textMuted
160
+ }], numberOfLines: 1, children: leadSource.name }),
161
+ sources.length > 1 ? /* @__PURE__ */ jsxs(Text, { style: [styles.leadPillText, {
169
162
  color: palette.textMuted
170
163
  }], children: [
171
- "Summarized from ",
172
- sourceCountLabel
173
- ] })
164
+ "+",
165
+ sources.length - 1
166
+ ] }) : null
167
+ ] }) : null,
168
+ /* @__PURE__ */ jsxs(View, { style: [styles.byline, {
169
+ borderBottomColor: palette.border
170
+ }], children: [
171
+ /* @__PURE__ */ jsxs(View, { style: styles.bylineItem, children: [
172
+ /* @__PURE__ */ jsx(View, { style: [styles.bylineIcon, {
173
+ backgroundColor: palette.primarySoft
174
+ }], children: /* @__PURE__ */ jsx(Ionicons, { name: "layers-outline", size: 13, color: palette.primary }) }),
175
+ /* @__PURE__ */ jsxs(Text, { style: [styles.bylineStrong, {
176
+ color: palette.textMuted
177
+ }], children: [
178
+ "Summarized from ",
179
+ sourceCountLabel
180
+ ] })
181
+ ] }),
182
+ publishedLabel ? /* @__PURE__ */ jsxs(Pressable, { accessibilityRole: "link", onPress: () => article.href && openLink(article.href), style: styles.bylineItem, children: [
183
+ /* @__PURE__ */ jsx(Ionicons, { name: "time-outline", size: 14, color: palette.textMuted }),
184
+ /* @__PURE__ */ jsx(Text, { style: [styles.bylineText, {
185
+ color: palette.textMuted
186
+ }], children: publishedLabel })
187
+ ] }) : null
174
188
  ] }),
175
- publishedLabel ? /* @__PURE__ */ jsxs(Pressable, { accessibilityRole: "link", onPress: () => article.href && openLink(article.href), style: styles.bylineItem, children: [
176
- /* @__PURE__ */ jsx(Ionicons, { name: "time-outline", size: 14, color: palette.textMuted }),
177
- /* @__PURE__ */ jsx(Text, { style: [styles.bylineText, {
178
- color: palette.textMuted
179
- }], children: publishedLabel })
180
- ] }) : null
189
+ /* @__PURE__ */ jsx(SourceCards, { sources, palette, onOpen: openLink }),
190
+ article.imageUrl ? /* @__PURE__ */ jsx(DiscoverCard, { palette, radius: 22, style: styles.hero, innerStyle: styles.heroInner, children: /* @__PURE__ */ jsx(Image, { source: {
191
+ uri: article.imageUrl
192
+ }, style: StyleSheet.absoluteFill, contentFit: "cover", transition: 150, accessibilityIgnoresInvertColors: true }) }) : null,
193
+ content ? /* @__PURE__ */ jsx(View, { style: styles.body, children: /* @__PURE__ */ jsx(AssistantMarkdown, { text: content, isDark }) }) : null,
194
+ /* @__PURE__ */ jsx(DiscoverMore, { article, palette, onOpen: onOpenStory })
181
195
  ] }),
182
- /* @__PURE__ */ jsx(SourceCards, { sources, palette, onOpen: openLink }),
183
- article.imageUrl ? /* @__PURE__ */ jsx(View, { style: [styles.hero, {
184
- backgroundColor: palette.imagePlaceholder,
185
- borderColor: palette.cardBorder
186
- }], children: /* @__PURE__ */ jsx(Image, { source: {
187
- uri: article.imageUrl
188
- }, style: StyleSheet.absoluteFill, contentFit: "cover", transition: 150, accessibilityIgnoresInvertColors: true }) }) : null,
189
- content ? /* @__PURE__ */ jsx(View, { style: styles.body, children: /* @__PURE__ */ jsx(AssistantMarkdown, { text: content, isDark }) }) : null,
190
- /* @__PURE__ */ jsx(DiscoverMore, { article, palette, onOpen: onOpenStory })
191
- ] });
196
+ /* @__PURE__ */ jsx(DiscoverFollowUp, { article, orgName, palette })
197
+ ] }) });
192
198
  }
193
199
  const styles = StyleSheet.create({
200
+ root: {
201
+ flex: 1
202
+ },
194
203
  content: {
195
204
  paddingHorizontal: 18,
196
205
  paddingTop: 18,
@@ -203,18 +212,17 @@ const styles = StyleSheet.create({
203
212
  flexDirection: "row"
204
213
  },
205
214
  title: {
206
- marginTop: 14,
215
+ marginTop: 16,
207
216
  fontFamily: DISCOVER_SERIF,
208
- fontSize: 28,
209
- lineHeight: 34,
210
- fontWeight: "700",
217
+ fontSize: 34,
218
+ lineHeight: 37,
211
219
  letterSpacing: -0.4
212
220
  },
213
221
  summary: {
214
- marginTop: 14,
215
- fontFamily: DISCOVER_SERIF,
216
- fontSize: 18,
217
- lineHeight: 27
222
+ marginTop: 20,
223
+ fontSize: 17,
224
+ lineHeight: 28,
225
+ opacity: 0.85
218
226
  },
219
227
  leadPill: {
220
228
  marginTop: 10,
@@ -267,15 +275,14 @@ const styles = StyleSheet.create({
267
275
  },
268
276
  sourcesContent: {
269
277
  paddingHorizontal: 18,
278
+ paddingVertical: 6,
270
279
  gap: 10
271
280
  },
272
281
  sourceCard: {
273
282
  width: 168,
274
- minHeight: 82,
275
- borderRadius: 12,
276
- borderWidth: StyleSheet.hairlineWidth,
277
- padding: 11,
278
- gap: 7
283
+ minHeight: 88,
284
+ padding: 12,
285
+ gap: 8
279
286
  },
280
287
  sourceHead: {
281
288
  flexDirection: "row",
@@ -297,19 +304,18 @@ const styles = StyleSheet.create({
297
304
  justifyContent: "center"
298
305
  },
299
306
  hero: {
300
- marginTop: 18,
307
+ marginTop: 18
308
+ },
309
+ heroInner: {
301
310
  width: "100%",
302
- aspectRatio: 16 / 9,
303
- borderRadius: 16,
304
- borderWidth: StyleSheet.hairlineWidth,
305
- overflow: "hidden"
311
+ aspectRatio: 16 / 9
306
312
  },
307
313
  body: {
308
314
  marginTop: 20
309
315
  },
310
316
  more: {
311
- marginTop: 32,
312
- paddingTop: 22,
317
+ marginTop: 40,
318
+ paddingTop: 28,
313
319
  borderTopWidth: StyleSheet.hairlineWidth
314
320
  },
315
321
  moreHead: {
@@ -318,13 +324,20 @@ const styles = StyleSheet.create({
318
324
  gap: 8
319
325
  },
320
326
  moreTitle: {
321
- fontSize: 19,
322
- fontWeight: "700",
323
- letterSpacing: -0.2
327
+ fontFamily: DISCOVER_SERIF,
328
+ fontSize: 26,
329
+ lineHeight: 30
324
330
  },
325
331
  moreList: {
326
- marginTop: 14,
327
- gap: 12
332
+ marginTop: 18,
333
+ gap: 16
334
+ },
335
+ moreRow: {
336
+ flexDirection: "row",
337
+ gap: 16
338
+ },
339
+ moreSpacer: {
340
+ flex: 1
328
341
  },
329
342
  state: {
330
343
  flex: 1,