@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.
- package/lib/features/agent-chat/NativeAgentChat.js +21 -4
- package/lib/features/agent-chat/NativeAgentChat.js.map +1 -1
- package/lib/features/chat/ApprovalCard.js +353 -0
- package/lib/features/chat/ApprovalCard.js.map +1 -0
- package/lib/features/chat/AskUserCard.js +9 -2
- package/lib/features/chat/AskUserCard.js.map +1 -1
- package/lib/features/chat/askUser.js +25 -3
- package/lib/features/chat/askUser.js.map +1 -1
- package/lib/features/chat/queuedFollowUpRows.js +13 -0
- package/lib/features/chat/queuedFollowUpRows.js.map +1 -0
- package/lib/features/devices/bodyComposition.js +116 -0
- package/lib/features/devices/bodyComposition.js.map +1 -0
- package/lib/features/devices/readings.js.map +1 -1
- package/lib/features/devices/session.js +22 -2
- package/lib/features/devices/session.js.map +1 -1
- package/lib/features/discover/DiscoverArticle.js +105 -92
- package/lib/features/discover/DiscoverArticle.js.map +1 -1
- package/lib/features/discover/DiscoverFeed.js +97 -48
- package/lib/features/discover/DiscoverFeed.js.map +1 -1
- package/lib/features/discover/DiscoverFollowUp.js +135 -0
- package/lib/features/discover/DiscoverFollowUp.js.map +1 -0
- package/lib/features/discover/DiscoverSidebar.js +555 -0
- package/lib/features/discover/DiscoverSidebar.js.map +1 -0
- package/lib/features/discover/DiscoverSkeletons.js +123 -0
- package/lib/features/discover/DiscoverSkeletons.js.map +1 -0
- package/lib/features/discover/DiscoverStoryCards.js +193 -87
- package/lib/features/discover/DiscoverStoryCards.js.map +1 -1
- package/lib/features/discover/followUpQuery.js +42 -0
- package/lib/features/discover/followUpQuery.js.map +1 -0
- package/lib/features/discover/markets.js +37 -0
- package/lib/features/discover/markets.js.map +1 -0
- package/lib/features/discover/useDiscoverSidebar.js +148 -0
- package/lib/features/discover/useDiscoverSidebar.js.map +1 -0
- package/lib/features/discover/weather.js +42 -0
- package/lib/features/discover/weather.js.map +1 -0
- package/lib/features/expenses/ExpensesAgentChatPane.js +1 -1
- package/lib/features/expenses/ExpensesAgentChatPane.js.map +1 -1
- package/lib/features/inbox/InboxAgentChatPane.js +1 -1
- package/lib/features/inbox/InboxAgentChatPane.js.map +1 -1
- package/lib/features/job-search/JobSearchAgentChatPane.js +1 -0
- package/lib/features/job-search/JobSearchAgentChatPane.js.map +1 -1
- package/lib/features/local-services/LocalServicesAgentChatPane.js +1 -1
- package/lib/features/local-services/LocalServicesAgentChatPane.js.map +1 -1
- package/lib/features/movies/MoviesAgentChatPane.js +1 -1
- package/lib/features/movies/MoviesAgentChatPane.js.map +1 -1
- package/lib/features/notion/NotionBlockSheets.js +206 -0
- package/lib/features/notion/NotionBlockSheets.js.map +1 -0
- package/lib/features/notion/NotionBlockView.js +36 -0
- package/lib/features/notion/NotionBlockView.js.map +1 -0
- package/lib/features/notion/NotionCommentsSheet.js +461 -0
- package/lib/features/notion/NotionCommentsSheet.js.map +1 -0
- package/lib/features/notion/NotionConnectionsSheet.js +244 -0
- package/lib/features/notion/NotionConnectionsSheet.js.map +1 -0
- package/lib/features/notion/NotionHistorySheet.js +439 -0
- package/lib/features/notion/NotionHistorySheet.js.map +1 -0
- package/lib/features/notion/NotionMediaBlocks.js +803 -0
- package/lib/features/notion/NotionMediaBlocks.js.map +1 -0
- package/lib/features/notion/NotionMentionSheet.js +279 -0
- package/lib/features/notion/NotionMentionSheet.js.map +1 -0
- package/lib/features/notion/NotionPageMenu.js +585 -0
- package/lib/features/notion/NotionPageMenu.js.map +1 -0
- package/lib/features/notion/NotionPageTree.js +220 -0
- package/lib/features/notion/NotionPageTree.js.map +1 -0
- package/lib/features/notion/NotionPageView.js +1253 -152
- package/lib/features/notion/NotionPageView.js.map +1 -1
- package/lib/features/notion/NotionShareSheet.js +606 -0
- package/lib/features/notion/NotionShareSheet.js.map +1 -0
- package/lib/features/notion/NotionSheets.js +87 -15
- package/lib/features/notion/NotionSheets.js.map +1 -1
- package/lib/features/notion/NotionSidebarSections.js +492 -0
- package/lib/features/notion/NotionSidebarSections.js.map +1 -0
- package/lib/features/notion/NotionWorkspaceAdmin.js +659 -0
- package/lib/features/notion/NotionWorkspaceAdmin.js.map +1 -0
- package/lib/features/notion/NotionWorkspaceView.js +257 -45
- package/lib/features/notion/NotionWorkspaceView.js.map +1 -1
- package/lib/features/notion/database/DatabaseCells.js +554 -0
- package/lib/features/notion/database/DatabaseCells.js.map +1 -0
- package/lib/features/notion/database/DatabaseDateLayouts.js +792 -0
- package/lib/features/notion/database/DatabaseDateLayouts.js.map +1 -0
- package/lib/features/notion/database/DatabaseLayouts.js +461 -0
- package/lib/features/notion/database/DatabaseLayouts.js.map +1 -0
- package/lib/features/notion/database/DatabaseSheets.js +791 -0
- package/lib/features/notion/database/DatabaseSheets.js.map +1 -0
- package/lib/features/notion/database/NotionDatabaseView.js +817 -0
- package/lib/features/notion/database/NotionDatabaseView.js.map +1 -0
- package/lib/features/notion/database/RowPropertiesPanel.js +162 -0
- package/lib/features/notion/database/RowPropertiesPanel.js.map +1 -0
- package/lib/features/notion/database/chartData.js +89 -0
- package/lib/features/notion/database/chartData.js.map +1 -0
- package/lib/features/notion/database/chartView.js +112 -0
- package/lib/features/notion/database/chartView.js.map +1 -0
- package/lib/features/notion/database/connectorButton.js +33 -0
- package/lib/features/notion/database/connectorButton.js.map +1 -0
- package/lib/features/notion/database/databaseCalc.js +208 -0
- package/lib/features/notion/database/databaseCalc.js.map +1 -0
- package/lib/features/notion/database/databaseCalendar.js +157 -0
- package/lib/features/notion/database/databaseCalendar.js.map +1 -0
- package/lib/features/notion/database/databaseCsv.js +98 -0
- package/lib/features/notion/database/databaseCsv.js.map +1 -0
- package/lib/features/notion/database/databaseCsvActions.js +58 -0
- package/lib/features/notion/database/databaseCsvActions.js.map +1 -0
- package/lib/features/notion/database/databaseFilter.js +473 -0
- package/lib/features/notion/database/databaseFilter.js.map +1 -0
- package/lib/features/notion/database/databaseFormula.js +1072 -0
- package/lib/features/notion/database/databaseFormula.js.map +1 -0
- package/lib/features/notion/database/databaseGroup.js +151 -0
- package/lib/features/notion/database/databaseGroup.js.map +1 -0
- package/lib/features/notion/database/databaseRollup.js +58 -0
- package/lib/features/notion/database/databaseRollup.js.map +1 -0
- package/lib/features/notion/database/databaseTimeline.js +156 -0
- package/lib/features/notion/database/databaseTimeline.js.map +1 -0
- package/lib/features/notion/database/databaseView.js +1188 -0
- package/lib/features/notion/database/databaseView.js.map +1 -0
- package/lib/features/notion/database/databaseViews.js +310 -0
- package/lib/features/notion/database/databaseViews.js.map +1 -0
- package/lib/features/notion/database/relationSource.js +92 -0
- package/lib/features/notion/database/relationSource.js.map +1 -0
- package/lib/features/notion/database/uniqueId.js +17 -0
- package/lib/features/notion/database/uniqueId.js.map +1 -0
- package/lib/features/notion/database/useNotionDatabase.js +617 -0
- package/lib/features/notion/database/useNotionDatabase.js.map +1 -0
- package/lib/features/notion/notionAgentRunStatus.js +39 -0
- package/lib/features/notion/notionAgentRunStatus.js.map +1 -0
- package/lib/features/notion/notionCodeLanguages.js +112 -0
- package/lib/features/notion/notionCodeLanguages.js.map +1 -0
- package/lib/features/notion/notionComments.js +86 -0
- package/lib/features/notion/notionComments.js.map +1 -0
- package/lib/features/notion/notionConnections.js +74 -0
- package/lib/features/notion/notionConnections.js.map +1 -0
- package/lib/features/notion/notionDocuments.js +453 -1
- package/lib/features/notion/notionDocuments.js.map +1 -1
- package/lib/features/notion/notionEditorModel.js +142 -7
- package/lib/features/notion/notionEditorModel.js.map +1 -1
- package/lib/features/notion/notionMarkdownShortcuts.js +51 -0
- package/lib/features/notion/notionMarkdownShortcuts.js.map +1 -0
- package/lib/features/notion/notionMedia.js +119 -0
- package/lib/features/notion/notionMedia.js.map +1 -0
- package/lib/features/notion/notionMentionDate.js +32 -0
- package/lib/features/notion/notionMentionDate.js.map +1 -0
- package/lib/features/notion/notionMentionOptions.js +162 -0
- package/lib/features/notion/notionMentionOptions.js.map +1 -0
- package/lib/features/notion/notionNotesAgent.js +445 -0
- package/lib/features/notion/notionNotesAgent.js.map +1 -0
- package/lib/features/notion/notionPageAccess.js +168 -0
- package/lib/features/notion/notionPageAccess.js.map +1 -0
- package/lib/features/notion/notionPageActions.js +138 -0
- package/lib/features/notion/notionPageActions.js.map +1 -0
- package/lib/features/notion/notionPageHistory.js +154 -0
- package/lib/features/notion/notionPageHistory.js.map +1 -0
- package/lib/features/notion/notionPageKinds.js +200 -0
- package/lib/features/notion/notionPageKinds.js.map +1 -0
- package/lib/features/notion/notionRecents.js +48 -0
- package/lib/features/notion/notionRecents.js.map +1 -0
- package/lib/features/notion/notionRichText.js +177 -0
- package/lib/features/notion/notionRichText.js.map +1 -0
- package/lib/features/notion/notionRichTitle.js +336 -0
- package/lib/features/notion/notionRichTitle.js.map +1 -0
- package/lib/features/notion/useNotionWorkspace.js +272 -13
- package/lib/features/notion/useNotionWorkspace.js.map +1 -1
- package/lib/features/notion/usePageActions.js +155 -0
- package/lib/features/notion/usePageActions.js.map +1 -0
- package/lib/features/resume/ResumeAgentChatPane.js +1 -1
- package/lib/features/resume/ResumeAgentChatPane.js.map +1 -1
- package/lib/features/shopping/ShoppingAgentChatPane.js +1 -1
- package/lib/features/shopping/ShoppingAgentChatPane.js.map +1 -1
- package/lib/features/study/StudyAgentChatPane.js +1 -1
- package/lib/features/study/StudyAgentChatPane.js.map +1 -1
- package/lib/features/travel/TravelAgentChatPane.js +1 -1
- package/lib/features/travel/TravelAgentChatPane.js.map +1 -1
- package/lib/features/webbuilder/WebBuilderExperience.js +40 -8
- package/lib/features/webbuilder/WebBuilderExperience.js.map +1 -1
- package/lib/features/webbuilder/buildFragments.js +1 -1
- package/lib/features/webbuilder/buildFragments.js.map +1 -1
- package/lib/features/webbuilder/codeFiles.js +302 -0
- package/lib/features/webbuilder/codeFiles.js.map +1 -0
- package/lib/features/webbuilder/codeHighlight.js +209 -0
- package/lib/features/webbuilder/codeHighlight.js.map +1 -0
- package/lib/features/webbuilder/components/CodeView.js +682 -52
- package/lib/features/webbuilder/components/CodeView.js.map +1 -1
- package/lib/features/webbuilder/components/PreviewScreen.js +59 -38
- package/lib/features/webbuilder/components/PreviewScreen.js.map +1 -1
- package/lib/features/webbuilder/components/ProjectChat.js +3 -3
- package/lib/features/webbuilder/components/ProjectChat.js.map +1 -1
- package/lib/features/webbuilder/stepLabels.js +68 -0
- package/lib/features/webbuilder/stepLabels.js.map +1 -0
- package/lib/hooks/useHostChatSteering.js +45 -0
- package/lib/hooks/useHostChatSteering.js.map +1 -0
- package/lib/screens/Chat/index.js +2 -12
- package/lib/screens/Chat/index.js.map +1 -1
- package/lib/screens/Devices/index.js +205 -3
- package/lib/screens/Devices/index.js.map +1 -1
- package/lib/screens/ExpensesNative/useExpensesHostChat.js +25 -5
- package/lib/screens/ExpensesNative/useExpensesHostChat.js.map +1 -1
- package/lib/screens/InboxNative/useInboxHostChat.js +25 -5
- package/lib/screens/InboxNative/useInboxHostChat.js.map +1 -1
- package/lib/screens/JobSearchNative/useJobSearchHostChat.js +25 -5
- package/lib/screens/JobSearchNative/useJobSearchHostChat.js.map +1 -1
- package/lib/screens/LocalServicesNative/useLocalServicesHostChat.js +25 -5
- package/lib/screens/LocalServicesNative/useLocalServicesHostChat.js.map +1 -1
- package/lib/screens/MoviesNative/useMoviesHostChat.js +25 -5
- package/lib/screens/MoviesNative/useMoviesHostChat.js.map +1 -1
- package/lib/screens/NotionPage/index.js +2 -2
- package/lib/screens/NotionPage/index.js.map +1 -1
- package/lib/screens/ResumeNative/useResumeHostChat.js +25 -5
- package/lib/screens/ResumeNative/useResumeHostChat.js.map +1 -1
- package/lib/screens/ShoppingNative/useShoppingHostChat.js +25 -5
- package/lib/screens/ShoppingNative/useShoppingHostChat.js.map +1 -1
- package/lib/screens/StudyNative/useStudyHostChat.js +25 -5
- package/lib/screens/StudyNative/useStudyHostChat.js.map +1 -1
- package/lib/screens/TravelNative/useTravelHostChat.js +25 -5
- package/lib/screens/TravelNative/useTravelHostChat.js.map +1 -1
- package/lib/screens/WebBuilderNative/codeWorkspaceApi.js +222 -0
- package/lib/screens/WebBuilderNative/codeWorkspaceApi.js.map +1 -0
- package/lib/screens/WebBuilderNative/index.js +1 -1
- package/lib/screens/WebBuilderNative/index.js.map +1 -1
- package/lib/screens/WebBuilderNative/sandboxProxyApi.js +126 -4
- package/lib/screens/WebBuilderNative/sandboxProxyApi.js.map +1 -1
- package/lib/screens/WebBuilderNative/useWebBuilderHost.js +120 -11
- package/lib/screens/WebBuilderNative/useWebBuilderHost.js.map +1 -1
- 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
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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:
|
|
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__ */
|
|
135
|
+
return /* @__PURE__ */ jsx(AppKeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : void 0, keyboardVerticalOffset: topOffset, style: [styles.root, {
|
|
137
136
|
backgroundColor: palette.bg
|
|
138
|
-
},
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
/* @__PURE__ */ jsx(Text, { style: [styles.
|
|
152
|
-
color: palette.
|
|
153
|
-
}],
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
"
|
|
172
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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(
|
|
183
|
-
|
|
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:
|
|
215
|
+
marginTop: 16,
|
|
207
216
|
fontFamily: DISCOVER_SERIF,
|
|
208
|
-
fontSize:
|
|
209
|
-
lineHeight:
|
|
210
|
-
fontWeight: "700",
|
|
217
|
+
fontSize: 34,
|
|
218
|
+
lineHeight: 37,
|
|
211
219
|
letterSpacing: -0.4
|
|
212
220
|
},
|
|
213
221
|
summary: {
|
|
214
|
-
marginTop:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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:
|
|
275
|
-
|
|
276
|
-
|
|
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:
|
|
312
|
-
paddingTop:
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
327
|
+
fontFamily: DISCOVER_SERIF,
|
|
328
|
+
fontSize: 26,
|
|
329
|
+
lineHeight: 30
|
|
324
330
|
},
|
|
325
331
|
moreList: {
|
|
326
|
-
marginTop:
|
|
327
|
-
gap:
|
|
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,
|