@adminide-stack/yantra-mobile 12.0.137 → 12.0.139

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 (89) hide show
  1. package/lib/components/CustomDrawer.js +17 -1
  2. package/lib/components/CustomDrawer.js.map +1 -1
  3. package/lib/compute.js +106 -1
  4. package/lib/compute.js.map +1 -1
  5. package/lib/config/env-config.js +12 -0
  6. package/lib/config/env-config.js.map +1 -1
  7. package/lib/features/chat/ChatTranscript.js +2 -2
  8. package/lib/features/chat/ChatTranscript.js.map +1 -1
  9. package/lib/features/chat/surfaceNotes.js +4 -0
  10. package/lib/features/chat/surfaceNotes.js.map +1 -0
  11. package/lib/features/dictate/backgroundRecording.js.map +1 -1
  12. package/lib/features/dictate/useDictationRecorder.js +42 -1
  13. package/lib/features/dictate/useDictationRecorder.js.map +1 -1
  14. package/lib/features/home/useHomeWeather.js +85 -0
  15. package/lib/features/home/useHomeWeather.js.map +1 -0
  16. package/lib/features/home/weather.js +139 -0
  17. package/lib/features/home/weather.js.map +1 -0
  18. package/lib/features/notion/NotionPageView.js +819 -0
  19. package/lib/features/notion/NotionPageView.js.map +1 -0
  20. package/lib/features/notion/NotionSheets.js +452 -0
  21. package/lib/features/notion/NotionSheets.js.map +1 -0
  22. package/lib/features/notion/NotionTrashSheet.js +415 -0
  23. package/lib/features/notion/NotionTrashSheet.js.map +1 -0
  24. package/lib/features/notion/NotionWorkspaceView.js +837 -0
  25. package/lib/features/notion/NotionWorkspaceView.js.map +1 -0
  26. package/lib/features/notion/notionBlocks.js +203 -0
  27. package/lib/features/notion/notionBlocks.js.map +1 -0
  28. package/lib/features/notion/notionDocuments.js +182 -0
  29. package/lib/features/notion/notionDocuments.js.map +1 -0
  30. package/lib/features/notion/notionEditorModel.js +180 -0
  31. package/lib/features/notion/notionEditorModel.js.map +1 -0
  32. package/lib/features/notion/notionTemplates.js +406 -0
  33. package/lib/features/notion/notionTemplates.js.map +1 -0
  34. package/lib/features/notion/notionTheme.js +34 -0
  35. package/lib/features/notion/notionTheme.js.map +1 -0
  36. package/lib/features/notion/notionTrash.js +100 -0
  37. package/lib/features/notion/notionTrash.js.map +1 -0
  38. package/lib/features/notion/notionUi.js +310 -0
  39. package/lib/features/notion/notionUi.js.map +1 -0
  40. package/lib/features/notion/useNotionWorkspace.js +181 -0
  41. package/lib/features/notion/useNotionWorkspace.js.map +1 -0
  42. package/lib/features/webbuilder/buildFragments.js +15 -9
  43. package/lib/features/webbuilder/buildFragments.js.map +1 -1
  44. package/lib/features/webbuilder/buildJob.js +3 -3
  45. package/lib/features/webbuilder/buildJob.js.map +1 -1
  46. package/lib/features/webbuilder/publicSandboxDomain.js +5 -0
  47. package/lib/features/webbuilder/publicSandboxDomain.js.map +1 -0
  48. package/lib/features/webbuilder/sandboxPublicHost.js +74 -0
  49. package/lib/features/webbuilder/sandboxPublicHost.js.map +1 -0
  50. package/lib/features/webbuilder/siteAddress.js +2 -1
  51. package/lib/features/webbuilder/siteAddress.js.map +1 -1
  52. package/lib/features/webbuilder/webbuilderPrompts.js +3 -2
  53. package/lib/features/webbuilder/webbuilderPrompts.js.map +1 -1
  54. package/lib/features/webbuilder/workflow.js +2 -2
  55. package/lib/features/webbuilder/workflow.js.map +1 -1
  56. package/lib/hooks/useChatApi.js +15 -3
  57. package/lib/hooks/useChatApi.js.map +1 -1
  58. package/lib/hooks/usePrerequisiteIds.js +15 -2
  59. package/lib/hooks/usePrerequisiteIds.js.map +1 -1
  60. package/lib/routes.json +107 -0
  61. package/lib/screens/Home/HomeScreen.js +20 -5
  62. package/lib/screens/Home/HomeScreen.js.map +1 -1
  63. package/lib/screens/Home/components/HomeHero.js +117 -0
  64. package/lib/screens/Home/components/HomeHero.js.map +1 -0
  65. package/lib/screens/Home/components/HomeSuggestions.js +108 -85
  66. package/lib/screens/Home/components/HomeSuggestions.js.map +1 -1
  67. package/lib/screens/Home/components/HomeWash.js +44 -0
  68. package/lib/screens/Home/components/HomeWash.js.map +1 -0
  69. package/lib/screens/Home/components/homeCatalog.js +125 -0
  70. package/lib/screens/Home/components/homeCatalog.js.map +1 -0
  71. package/lib/screens/Notion/index.js +18 -0
  72. package/lib/screens/Notion/index.js.map +1 -0
  73. package/lib/screens/NotionPage/index.js +19 -0
  74. package/lib/screens/NotionPage/index.js.map +1 -0
  75. package/lib/screens/WebBuilderNative/sandboxProxyApi.js +2 -2
  76. package/lib/screens/WebBuilderNative/sandboxProxyApi.js.map +1 -1
  77. package/lib/screens/WebBuilderNative/useRestoredSandboxes.js +16 -8
  78. package/lib/screens/WebBuilderNative/useRestoredSandboxes.js.map +1 -1
  79. package/lib/screens/WebBuilderNative/useWebBuilderGallery.js +6 -5
  80. package/lib/screens/WebBuilderNative/useWebBuilderGallery.js.map +1 -1
  81. package/lib/screens/WebBuilderNative/useWebBuilderHost.js +5 -4
  82. package/lib/screens/WebBuilderNative/useWebBuilderHost.js.map +1 -1
  83. package/lib/screens/WebBuilderNative/webbuilderTransport.js +4 -3
  84. package/lib/screens/WebBuilderNative/webbuilderTransport.js.map +1 -1
  85. package/lib/services/savedWorkspace.js +44 -0
  86. package/lib/services/savedWorkspace.js.map +1 -0
  87. package/lib/theme/mobileTokens.js +1 -0
  88. package/lib/theme/mobileTokens.js.map +1 -1
  89. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"ChatTranscript.js","sources":["../../../src/features/chat/ChatTranscript.tsx"],"sourcesContent":["/**\n * Chat thread list that can show attachment previews.\n *\n * `@messenger-box/platform-mobile` PlanModeView maps each row to GiftedChat\n * `{ text }` only, so an image send becomes the gateway caption\n * \"Attached: IMG_0005.jpg\" with no thumbnail. This list keeps streaming\n * markdown for the assistant and draws Manus-style previews on the user side.\n *\n * Rows stay oldest → newest (same as web). A previous `inverted` FlatList\n * reversed that order on device. The composer dock already lifts with the\n * keyboard; this list just fills the leftover space and pins to the latest\n * turn.\n */\nimport React, { memo, useCallback, useEffect, useMemo, useRef, type ReactNode } from 'react';\nimport { FlatList, Platform, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';\nimport Markdown from 'react-native-markdown-display';\nimport { dismissKeyboard } from '../../utils/keyboardController';\nimport { useKeyboardLiftHeight } from '../../components/KeyboardComposerDock';\nimport type { MessageAttachment } from '../../hooks/useChatStream';\nimport { displayUserMessageText, shouldRenderUserTranscriptTurn } from '../attachments/displayUserMessageText';\nimport { MessageAttachmentPreviews } from '../attachments/MessageAttachmentPreviews';\nimport { ToolTimeline } from './ToolTimeline';\nimport { ChatWidgetCard } from './ChatWidgetCard';\nimport { splitChatWidgets } from './chatWidgets';\nimport { parseToolActivity } from './toolActivity';\nimport { parseAskUser, stripAskUser, displayAskUserAssistantText } from './askUser';\nimport { collapseDuplicateUserRows, groupConsecutiveRoleRows, type RoleGroup } from './transcriptGroups';\nimport { AssistantMarkdown } from './AssistantMarkdown';\nimport { useChatLinkOpener } from '../links/chatLinkOpener';\nimport { autolinkBareUrls } from '../messaging/chatLinks';\nimport { PreResponseStrip } from './PreResponseStrip';\nimport { splitPreResponse } from './preResponse';\n\nexport type TranscriptMessage = {\n id: string;\n role: string;\n content: string;\n displayContent?: string;\n attachments?: MessageAttachment[];\n /** Typed while the gateway was down: shown in place, waiting on the outbox. */\n queued?: boolean;\n /** What the caption under a queued turn says, when it is not waiting on the gateway to connect. */\n queuedNote?: string;\n};\n\ntype TranscriptRow = TranscriptMessage & {\n streaming?: boolean;\n askAnswered?: boolean;\n /** The fast lane's quick answer for this turn, drawn as a strip - never as text. */\n preResponse?: string | null;\n};\ntype TranscriptGroup = RoleGroup<TranscriptRow>;\n\nexport const USER_BUBBLE_BG = '#2563eb';\n\nexport function ChatTranscript({\n messages,\n streamingContent,\n quickReply,\n renderMessageActions,\n listContentStyle,\n listFooter,\n isDark = false,\n}: {\n messages: TranscriptMessage[];\n streamingContent?: string;\n /** The running turn's quick answer (fast lane or brain), already split off the stream. */\n quickReply?: string;\n renderMessageActions?: (message: { id: string; role: string; content: string }) => React.ReactNode;\n listContentStyle?: StyleProp<ViewStyle>;\n listFooter?: ReactNode;\n isDark?: boolean;\n}) {\n const listRef = useRef<FlatList<TranscriptGroup>>(null);\n const stickToBottomRef = useRef(true);\n const isProgrammaticScrollRef = useRef(false);\n const ignoreScrollUntilRef = useRef(0);\n const keyboardHeight = useKeyboardLiftHeight();\n\n const historyRows = useMemo<TranscriptRow[]>(() => {\n const rows: TranscriptRow[] = [];\n messages.forEach((msg, index) => {\n if (msg.role === 'user') {\n if (shouldRenderUserTranscriptTurn(msg.content, msg.attachments)) {\n rows.push({ ...msg, streaming: false });\n }\n return;\n }\n if (msg.role !== 'assistant') {\n rows.push({ ...msg, streaming: false });\n return;\n }\n // A persisted reply carries its quick answer prepended in the gateway's\n // markers; the row keeps the two apart from here on.\n const { preResponse, text: content } = splitPreResponse(msg.content);\n const hasAttachments = (msg.attachments?.length ?? 0) > 0;\n const askAnswered =\n Boolean(parseAskUser(content)) && messages.slice(index + 1).some((m) => m.role === 'user');\n const visible = displayAskUserAssistantText(content, askAnswered).trim();\n const tools = parseToolActivity(stripAskUser(content), false);\n const hasAsk = Boolean(parseAskUser(content));\n if (hasAsk && !visible && tools.steps.length === 0 && !hasAttachments && !preResponse) {\n return;\n }\n if (!hasAsk) {\n const leftover = stripAskUser(content).trim();\n if (!leftover && !hasAttachments && !preResponse) return;\n }\n rows.push({ ...msg, content, preResponse, streaming: false, askAnswered });\n });\n return rows;\n }, [messages]);\n\n const rows = useMemo<TranscriptRow[]>(() => {\n const history = collapseDuplicateUserRows(historyRows);\n // The chat hook hands the quick answer over split off the stream; a raw\n // stream (another host) may still carry the markers, open mid-stream.\n const { preResponse: streamPre, text: rawStream } = splitPreResponse(streamingContent ?? '');\n const livePre = quickReply?.trim() || streamPre;\n const last = history[history.length - 1];\n const streamAskAnswered = last?.role === 'user' && Boolean(parseAskUser(rawStream));\n const streamProse = displayAskUserAssistantText(rawStream, streamAskAnswered).trim();\n const streamTools = parseToolActivity(stripAskUser(rawStream), true);\n if (!streamProse && streamTools.steps.length === 0 && !livePre) return history;\n if (last?.role === 'assistant' && (!livePre || last.preResponse === livePre)) {\n const lastText = last.content.trim();\n const streamText = rawStream.trim();\n if (lastText === streamText || lastText.startsWith(streamText) || streamText.startsWith(lastText)) {\n return history;\n }\n }\n return [\n ...history,\n {\n id: '__streaming__',\n role: 'assistant',\n content: rawStream,\n preResponse: livePre,\n streaming: true,\n askAnswered: streamAskAnswered,\n },\n ];\n }, [historyRows, streamingContent, quickReply]);\n\n const groups = useMemo(() => groupConsecutiveRoleRows(rows), [rows]);\n\n const scrollToLatest = useCallback(\n (animated: boolean) => {\n if (groups.length === 0) return;\n isProgrammaticScrollRef.current = true;\n stickToBottomRef.current = true;\n const list = listRef.current;\n if (!list) return;\n list.scrollToEnd({ animated });\n },\n [groups.length],\n );\n\n useEffect(() => {\n if (groups.length === 0 || !stickToBottomRef.current) return undefined;\n const t = setTimeout(() => scrollToLatest(false), 0);\n return () => clearTimeout(t);\n }, [groups.length, streamingContent?.length, quickReply?.length, scrollToLatest]);\n\n // Spacer and list height update on the same keyboard frame. Scroll only\n // after that shrink, or we stay on the top of a long turn (image 1).\n useEffect(() => {\n if (keyboardHeight <= 0) return undefined;\n stickToBottomRef.current = true;\n ignoreScrollUntilRef.current = Date.now() + 400;\n const frame = requestAnimationFrame(() => scrollToLatest(false));\n const afterLayout = setTimeout(() => scrollToLatest(false), 64);\n const afterKeyboard = setTimeout(() => scrollToLatest(false), 280);\n return () => {\n cancelAnimationFrame(frame);\n clearTimeout(afterLayout);\n clearTimeout(afterKeyboard);\n };\n }, [keyboardHeight, scrollToLatest]);\n\n const handleScroll = useCallback(\n (e: {\n nativeEvent: {\n contentOffset: { y: number };\n contentSize: { height: number };\n layoutMeasurement: { height: number };\n };\n }) => {\n if (Date.now() < ignoreScrollUntilRef.current) {\n stickToBottomRef.current = true;\n return;\n }\n const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;\n const nearLatest = contentOffset.y + layoutMeasurement.height >= contentSize.height - 80;\n if (!isProgrammaticScrollRef.current) {\n stickToBottomRef.current = nearLatest;\n } else if (nearLatest) {\n isProgrammaticScrollRef.current = false;\n }\n },\n [],\n );\n\n const handleContentSizeChange = useCallback(() => {\n if (!stickToBottomRef.current) return;\n scrollToLatest(false);\n }, [scrollToLatest]);\n\n const renderItem = useCallback(\n ({ item }: { item: TranscriptGroup }) => (\n <TranscriptGroupView group={item} isDark={isDark} renderMessageActions={renderMessageActions} />\n ),\n [isDark, renderMessageActions],\n );\n\n return (\n <View style={styles.list}>\n <FlatList\n ref={listRef}\n data={groups}\n keyExtractor={(item) => item.id}\n renderItem={renderItem}\n extraData={`${streamingContent ?? ''}:${quickReply ?? ''}:${Boolean(listFooter)}`}\n ListFooterComponent={listFooter ? <>{listFooter}</> : null}\n initialNumToRender={12}\n maxToRenderPerBatch={8}\n updateCellsBatchingPeriod={50}\n windowSize={21}\n scrollEnabled\n nestedScrollEnabled={false}\n directionalLockEnabled\n onScroll={handleScroll}\n onContentSizeChange={handleContentSizeChange}\n onLayout={() => {\n if (stickToBottomRef.current) scrollToLatest(false);\n }}\n scrollEventThrottle={16}\n keyboardShouldPersistTaps=\"handled\"\n keyboardDismissMode={Platform.OS === 'ios' ? 'interactive' : 'on-drag'}\n automaticallyAdjustKeyboardInsets={false}\n automaticallyAdjustContentInsets={false}\n contentInsetAdjustmentBehavior=\"never\"\n contentContainerStyle={[styles.listContent, listContentStyle, !listFooter && styles.listContentFill]}\n style={styles.list}\n />\n </View>\n );\n}\n\nconst TranscriptGroupView = memo(function TranscriptGroupView({\n group,\n isDark,\n renderMessageActions,\n}: {\n group: TranscriptGroup;\n isDark: boolean;\n renderMessageActions?: (message: { id: string; role: string; content: string }) => React.ReactNode;\n}) {\n // A link the person pasted opens like one the assistant sent: in the app or a browser sheet.\n const openLink = useChatLinkOpener();\n const onLinkPress = useCallback(\n (url: string) => {\n openLink(url);\n return false;\n },\n [openLink],\n );\n if (group.role === 'user') {\n return (\n <Pressable style={styles.userCol} onPress={dismissKeyboard} accessible={false}>\n {group.items.map((item, index) => {\n const attachments = item.attachments ?? [];\n const text = displayUserMessageText(item.displayContent ?? item.content, attachments);\n if (!text && attachments.length === 0) return null;\n return (\n <View key={item.id} style={[styles.userTurn, index > 0 && styles.userFollow]}>\n {attachments.length > 0 ? (\n <MessageAttachmentPreviews attachments={attachments} align=\"end\" isDark={isDark} />\n ) : null}\n {text ? (\n <View style={[styles.userBubble, item.queued && styles.userBubbleQueued]}>\n <Markdown style={USER_MARKDOWN} onLinkPress={onLinkPress}>\n {/* Bare URLs the person typed become links, as in the assistant's replies. */}\n {autolinkBareUrls(text)}\n </Markdown>\n </View>\n ) : null}\n {item.queued ? (\n <Text style={[styles.queuedCaption, { color: isDark ? '#94a3b8' : '#64748b' }]}>\n {item.queuedNote ?? 'Queued · sends when the gateway connects'}\n </Text>\n ) : null}\n </View>\n );\n })}\n </Pressable>\n );\n }\n\n const last = group.items[group.items.length - 1];\n\n return (\n <View style={styles.assistantGroup}>\n {group.items.map((item, index) => (\n <AssistantTurnView\n key={item.id}\n item={item}\n isDark={isDark}\n tightTop={index > 0}\n renderMessageActions={item.streaming || item !== last ? undefined : renderMessageActions}\n />\n ))}\n </View>\n );\n});\n\nconst AssistantTurnView = memo(function AssistantTurnView({\n item,\n isDark,\n tightTop,\n renderMessageActions,\n}: {\n item: TranscriptRow;\n isDark: boolean;\n tightTop: boolean;\n renderMessageActions?: (message: { id: string; role: string; content: string }) => React.ReactNode;\n}) {\n const attachments = item.attachments ?? [];\n const stripped = stripAskUser(item.content);\n const toolActivity = parseToolActivity(stripped, !!item.streaming);\n // Widget envelopes become native cards below the text, never raw JSON.\n const { text, widgets } = splitChatWidgets(\n parseToolActivity(displayAskUserAssistantText(item.content, !!item.askAnswered), !!item.streaming).text,\n !!item.streaming,\n );\n\n // The quick answer stays open while it is all there is to read: until the\n // agent's own words start on a live turn, or for good on a reply that never\n // got anything past it. Tool steps alone do not fold it - they are not an answer.\n const preResponseOpen = item.streaming\n ? !text.trim()\n : !text.trim() && toolActivity.steps.length === 0 && widgets.length === 0 && attachments.length === 0;\n\n return (\n <View style={[styles.assistantCol, tightTop && styles.assistantColTight]}>\n {attachments.length > 0 ? (\n <MessageAttachmentPreviews attachments={attachments} align=\"start\" isDark={isDark} />\n ) : null}\n {item.preResponse ? (\n <PreResponseStrip\n text={item.preResponse}\n expanded={preResponseOpen}\n live={!!item.streaming}\n isDark={isDark}\n />\n ) : null}\n {toolActivity.steps.length > 0 ? (\n <ToolTimeline steps={toolActivity.steps} active={!!item.streaming} isDark={isDark} />\n ) : null}\n {text ? <AssistantMarkdown text={text} streaming={!!item.streaming} isDark={isDark} /> : null}\n {widgets.map((widget, i) => (\n <ChatWidgetCard key={`${widget.component}-${i}`} widget={widget} isDark={isDark} />\n ))}\n {renderMessageActions\n ? renderMessageActions({\n id: item.id,\n role: 'assistant',\n // Copy and share take what the user reads, not the wire markup.\n content: text.trim() || item.preResponse || item.content,\n })\n : null}\n </View>\n );\n});\n\nconst USER_MARKDOWN = {\n body: { color: '#ffffff', fontSize: 15, lineHeight: 22 },\n // White and underlined: the bubble is blue, so the usual blue link would vanish.\n link: { color: '#ffffff', textDecorationLine: 'underline' as const },\n paragraph: { marginTop: 4, marginBottom: 4, color: '#ffffff' },\n strong: { fontWeight: '700' as const, color: '#ffffff' },\n em: { color: '#ffffff' },\n text: { color: '#ffffff' },\n bullet_list: { marginVertical: 4 },\n ordered_list: { marginVertical: 4 },\n list_item: { marginVertical: 2, color: '#ffffff' },\n};\n\nconst styles = StyleSheet.create({\n list: {\n flex: 1,\n minHeight: 0,\n },\n listContent: {\n paddingTop: 8,\n paddingBottom: 8,\n paddingHorizontal: 8,\n },\n listContentFill: {\n flexGrow: 1,\n },\n userCol: {\n width: '100%',\n alignItems: 'flex-end',\n marginBottom: 12,\n paddingHorizontal: 4,\n },\n userTurn: {\n maxWidth: '92%',\n alignItems: 'flex-end',\n },\n userFollow: {\n marginTop: 4,\n },\n userBubble: {\n backgroundColor: USER_BUBBLE_BG,\n borderRadius: 16,\n paddingHorizontal: 12,\n paddingVertical: 8,\n },\n // Same bubble, held back: a queued turn reads as pending, not as delivered.\n userBubbleQueued: {\n opacity: 0.55,\n },\n queuedCaption: {\n fontSize: 11,\n fontWeight: '500',\n marginTop: 4,\n marginRight: 2,\n },\n assistantGroup: {\n width: '100%',\n marginBottom: 4,\n },\n assistantCol: {\n width: '100%',\n flexGrow: 0,\n alignItems: 'stretch',\n marginBottom: 8,\n paddingHorizontal: 4,\n },\n assistantColTight: {\n marginTop: -4,\n },\n});\n"],"names":["rows","TranscriptGroupView","AssistantTurnView"],"mappings":"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"}
1
+ {"version":3,"file":"ChatTranscript.js","sources":["../../../src/features/chat/ChatTranscript.tsx"],"sourcesContent":["/**\n * Chat thread list that can show attachment previews.\n *\n * `@messenger-box/platform-mobile` PlanModeView maps each row to GiftedChat\n * `{ text }` only, so an image send becomes the gateway caption\n * \"Attached: IMG_0005.jpg\" with no thumbnail. This list keeps streaming\n * markdown for the assistant and draws Manus-style previews on the user side.\n *\n * Rows stay oldest → newest (same as web). A previous `inverted` FlatList\n * reversed that order on device. The composer dock already lifts with the\n * keyboard; this list just fills the leftover space and pins to the latest\n * turn.\n */\nimport React, { memo, useCallback, useEffect, useMemo, useRef, type ReactNode } from 'react';\nimport { FlatList, Platform, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';\nimport Markdown from 'react-native-markdown-display';\nimport { dismissKeyboard } from '../../utils/keyboardController';\nimport { useKeyboardLiftHeight } from '../../components/KeyboardComposerDock';\nimport type { MessageAttachment } from '../../hooks/useChatStream';\nimport { displayUserMessageText, shouldRenderUserTranscriptTurn } from '../attachments/displayUserMessageText';\nimport { MessageAttachmentPreviews } from '../attachments/MessageAttachmentPreviews';\nimport { ToolTimeline } from './ToolTimeline';\nimport { ChatWidgetCard } from './ChatWidgetCard';\nimport { splitChatWidgets } from './chatWidgets';\nimport { parseToolActivity } from './toolActivity';\nimport { parseAskUser, stripAskUser, displayAskUserAssistantText } from './askUser';\nimport { stripSurfaceNotes } from './surfaceNotes';\nimport { collapseDuplicateUserRows, groupConsecutiveRoleRows, type RoleGroup } from './transcriptGroups';\nimport { AssistantMarkdown } from './AssistantMarkdown';\nimport { useChatLinkOpener } from '../links/chatLinkOpener';\nimport { autolinkBareUrls } from '../messaging/chatLinks';\nimport { PreResponseStrip } from './PreResponseStrip';\nimport { splitPreResponse } from './preResponse';\n\nexport type TranscriptMessage = {\n id: string;\n role: string;\n content: string;\n displayContent?: string;\n attachments?: MessageAttachment[];\n /** Typed while the gateway was down: shown in place, waiting on the outbox. */\n queued?: boolean;\n /** What the caption under a queued turn says, when it is not waiting on the gateway to connect. */\n queuedNote?: string;\n};\n\ntype TranscriptRow = TranscriptMessage & {\n streaming?: boolean;\n askAnswered?: boolean;\n /** The fast lane's quick answer for this turn, drawn as a strip - never as text. */\n preResponse?: string | null;\n};\ntype TranscriptGroup = RoleGroup<TranscriptRow>;\n\nexport const USER_BUBBLE_BG = '#2563eb';\n\nexport function ChatTranscript({\n messages,\n streamingContent,\n quickReply,\n renderMessageActions,\n listContentStyle,\n listFooter,\n isDark = false,\n}: {\n messages: TranscriptMessage[];\n streamingContent?: string;\n /** The running turn's quick answer (fast lane or brain), already split off the stream. */\n quickReply?: string;\n renderMessageActions?: (message: { id: string; role: string; content: string }) => React.ReactNode;\n listContentStyle?: StyleProp<ViewStyle>;\n listFooter?: ReactNode;\n isDark?: boolean;\n}) {\n const listRef = useRef<FlatList<TranscriptGroup>>(null);\n const stickToBottomRef = useRef(true);\n const isProgrammaticScrollRef = useRef(false);\n const ignoreScrollUntilRef = useRef(0);\n const keyboardHeight = useKeyboardLiftHeight();\n\n const historyRows = useMemo<TranscriptRow[]>(() => {\n const rows: TranscriptRow[] = [];\n messages.forEach((msg, index) => {\n if (msg.role === 'user') {\n if (shouldRenderUserTranscriptTurn(msg.content, msg.attachments)) {\n rows.push({ ...msg, streaming: false });\n }\n return;\n }\n if (msg.role !== 'assistant') {\n rows.push({ ...msg, streaming: false });\n return;\n }\n // A persisted reply carries its quick answer prepended in the gateway's\n // markers; the row keeps the two apart from here on.\n const { preResponse, text: content } = splitPreResponse(msg.content);\n const hasAttachments = (msg.attachments?.length ?? 0) > 0;\n const askAnswered =\n Boolean(parseAskUser(content)) && messages.slice(index + 1).some((m) => m.role === 'user');\n const visible = displayAskUserAssistantText(content, askAnswered).trim();\n const tools = parseToolActivity(stripAskUser(content), false);\n const hasAsk = Boolean(parseAskUser(content));\n if (hasAsk && !visible && tools.steps.length === 0 && !hasAttachments && !preResponse) {\n return;\n }\n if (!hasAsk) {\n const leftover = stripAskUser(content).trim();\n if (!leftover && !hasAttachments && !preResponse) return;\n }\n rows.push({ ...msg, content, preResponse, streaming: false, askAnswered });\n });\n return rows;\n }, [messages]);\n\n const rows = useMemo<TranscriptRow[]>(() => {\n const history = collapseDuplicateUserRows(historyRows);\n // The chat hook hands the quick answer over split off the stream; a raw\n // stream (another host) may still carry the markers, open mid-stream.\n const { preResponse: streamPre, text: rawStream } = splitPreResponse(streamingContent ?? '');\n const livePre = quickReply?.trim() || streamPre;\n const last = history[history.length - 1];\n const streamAskAnswered = last?.role === 'user' && Boolean(parseAskUser(rawStream));\n const streamProse = displayAskUserAssistantText(rawStream, streamAskAnswered).trim();\n const streamTools = parseToolActivity(stripAskUser(rawStream), true);\n if (!streamProse && streamTools.steps.length === 0 && !livePre) return history;\n if (last?.role === 'assistant' && (!livePre || last.preResponse === livePre)) {\n const lastText = last.content.trim();\n const streamText = rawStream.trim();\n if (lastText === streamText || lastText.startsWith(streamText) || streamText.startsWith(lastText)) {\n return history;\n }\n }\n return [\n ...history,\n {\n id: '__streaming__',\n role: 'assistant',\n content: rawStream,\n preResponse: livePre,\n streaming: true,\n askAnswered: streamAskAnswered,\n },\n ];\n }, [historyRows, streamingContent, quickReply]);\n\n const groups = useMemo(() => groupConsecutiveRoleRows(rows), [rows]);\n\n const scrollToLatest = useCallback(\n (animated: boolean) => {\n if (groups.length === 0) return;\n isProgrammaticScrollRef.current = true;\n stickToBottomRef.current = true;\n const list = listRef.current;\n if (!list) return;\n list.scrollToEnd({ animated });\n },\n [groups.length],\n );\n\n useEffect(() => {\n if (groups.length === 0 || !stickToBottomRef.current) return undefined;\n const t = setTimeout(() => scrollToLatest(false), 0);\n return () => clearTimeout(t);\n }, [groups.length, streamingContent?.length, quickReply?.length, scrollToLatest]);\n\n // Spacer and list height update on the same keyboard frame. Scroll only\n // after that shrink, or we stay on the top of a long turn (image 1).\n useEffect(() => {\n if (keyboardHeight <= 0) return undefined;\n stickToBottomRef.current = true;\n ignoreScrollUntilRef.current = Date.now() + 400;\n const frame = requestAnimationFrame(() => scrollToLatest(false));\n const afterLayout = setTimeout(() => scrollToLatest(false), 64);\n const afterKeyboard = setTimeout(() => scrollToLatest(false), 280);\n return () => {\n cancelAnimationFrame(frame);\n clearTimeout(afterLayout);\n clearTimeout(afterKeyboard);\n };\n }, [keyboardHeight, scrollToLatest]);\n\n const handleScroll = useCallback(\n (e: {\n nativeEvent: {\n contentOffset: { y: number };\n contentSize: { height: number };\n layoutMeasurement: { height: number };\n };\n }) => {\n if (Date.now() < ignoreScrollUntilRef.current) {\n stickToBottomRef.current = true;\n return;\n }\n const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;\n const nearLatest = contentOffset.y + layoutMeasurement.height >= contentSize.height - 80;\n if (!isProgrammaticScrollRef.current) {\n stickToBottomRef.current = nearLatest;\n } else if (nearLatest) {\n isProgrammaticScrollRef.current = false;\n }\n },\n [],\n );\n\n const handleContentSizeChange = useCallback(() => {\n if (!stickToBottomRef.current) return;\n scrollToLatest(false);\n }, [scrollToLatest]);\n\n const renderItem = useCallback(\n ({ item }: { item: TranscriptGroup }) => (\n <TranscriptGroupView group={item} isDark={isDark} renderMessageActions={renderMessageActions} />\n ),\n [isDark, renderMessageActions],\n );\n\n return (\n <View style={styles.list}>\n <FlatList\n ref={listRef}\n data={groups}\n keyExtractor={(item) => item.id}\n renderItem={renderItem}\n extraData={`${streamingContent ?? ''}:${quickReply ?? ''}:${Boolean(listFooter)}`}\n ListFooterComponent={listFooter ? <>{listFooter}</> : null}\n initialNumToRender={12}\n maxToRenderPerBatch={8}\n updateCellsBatchingPeriod={50}\n windowSize={21}\n scrollEnabled\n nestedScrollEnabled={false}\n directionalLockEnabled\n onScroll={handleScroll}\n onContentSizeChange={handleContentSizeChange}\n onLayout={() => {\n if (stickToBottomRef.current) scrollToLatest(false);\n }}\n scrollEventThrottle={16}\n keyboardShouldPersistTaps=\"handled\"\n keyboardDismissMode={Platform.OS === 'ios' ? 'interactive' : 'on-drag'}\n automaticallyAdjustKeyboardInsets={false}\n automaticallyAdjustContentInsets={false}\n contentInsetAdjustmentBehavior=\"never\"\n contentContainerStyle={[styles.listContent, listContentStyle, !listFooter && styles.listContentFill]}\n style={styles.list}\n />\n </View>\n );\n}\n\nconst TranscriptGroupView = memo(function TranscriptGroupView({\n group,\n isDark,\n renderMessageActions,\n}: {\n group: TranscriptGroup;\n isDark: boolean;\n renderMessageActions?: (message: { id: string; role: string; content: string }) => React.ReactNode;\n}) {\n // A link the person pasted opens like one the assistant sent: in the app or a browser sheet.\n const openLink = useChatLinkOpener();\n const onLinkPress = useCallback(\n (url: string) => {\n openLink(url);\n return false;\n },\n [openLink],\n );\n if (group.role === 'user') {\n return (\n <Pressable style={styles.userCol} onPress={dismissKeyboard} accessible={false}>\n {group.items.map((item, index) => {\n const attachments = item.attachments ?? [];\n const text = displayUserMessageText(item.displayContent ?? item.content, attachments);\n if (!text && attachments.length === 0) return null;\n return (\n <View key={item.id} style={[styles.userTurn, index > 0 && styles.userFollow]}>\n {attachments.length > 0 ? (\n <MessageAttachmentPreviews attachments={attachments} align=\"end\" isDark={isDark} />\n ) : null}\n {text ? (\n <View style={[styles.userBubble, item.queued && styles.userBubbleQueued]}>\n <Markdown style={USER_MARKDOWN} onLinkPress={onLinkPress}>\n {/* Bare URLs the person typed become links, as in the assistant's replies. */}\n {autolinkBareUrls(text)}\n </Markdown>\n </View>\n ) : null}\n {item.queued ? (\n <Text style={[styles.queuedCaption, { color: isDark ? '#94a3b8' : '#64748b' }]}>\n {item.queuedNote ?? 'Queued · sends when the gateway connects'}\n </Text>\n ) : null}\n </View>\n );\n })}\n </Pressable>\n );\n }\n\n const last = group.items[group.items.length - 1];\n\n return (\n <View style={styles.assistantGroup}>\n {group.items.map((item, index) => (\n <AssistantTurnView\n key={item.id}\n item={item}\n isDark={isDark}\n tightTop={index > 0}\n renderMessageActions={item.streaming || item !== last ? undefined : renderMessageActions}\n />\n ))}\n </View>\n );\n});\n\nconst AssistantTurnView = memo(function AssistantTurnView({\n item,\n isDark,\n tightTop,\n renderMessageActions,\n}: {\n item: TranscriptRow;\n isDark: boolean;\n tightTop: boolean;\n renderMessageActions?: (message: { id: string; role: string; content: string }) => React.ReactNode;\n}) {\n const attachments = item.attachments ?? [];\n const stripped = stripAskUser(item.content);\n const toolActivity = parseToolActivity(stripped, !!item.streaming);\n // Widget envelopes become native cards below the text, never raw JSON.\n const { text, widgets } = splitChatWidgets(\n parseToolActivity(\n stripSurfaceNotes(displayAskUserAssistantText(item.content, !!item.askAnswered)),\n !!item.streaming,\n ).text,\n !!item.streaming,\n );\n\n // The quick answer stays open while it is all there is to read: until the\n // agent's own words start on a live turn, or for good on a reply that never\n // got anything past it. Tool steps alone do not fold it - they are not an answer.\n const preResponseOpen = item.streaming\n ? !text.trim()\n : !text.trim() && toolActivity.steps.length === 0 && widgets.length === 0 && attachments.length === 0;\n\n return (\n <View style={[styles.assistantCol, tightTop && styles.assistantColTight]}>\n {attachments.length > 0 ? (\n <MessageAttachmentPreviews attachments={attachments} align=\"start\" isDark={isDark} />\n ) : null}\n {item.preResponse ? (\n <PreResponseStrip\n text={item.preResponse}\n expanded={preResponseOpen}\n live={!!item.streaming}\n isDark={isDark}\n />\n ) : null}\n {toolActivity.steps.length > 0 ? (\n <ToolTimeline steps={toolActivity.steps} active={!!item.streaming} isDark={isDark} />\n ) : null}\n {text ? <AssistantMarkdown text={text} streaming={!!item.streaming} isDark={isDark} /> : null}\n {widgets.map((widget, i) => (\n <ChatWidgetCard key={`${widget.component}-${i}`} widget={widget} isDark={isDark} />\n ))}\n {renderMessageActions\n ? renderMessageActions({\n id: item.id,\n role: 'assistant',\n // Copy and share take what the user reads, not the wire markup.\n content: text.trim() || item.preResponse || item.content,\n })\n : null}\n </View>\n );\n});\n\nconst USER_MARKDOWN = {\n body: { color: '#ffffff', fontSize: 15, lineHeight: 22 },\n // White and underlined: the bubble is blue, so the usual blue link would vanish.\n link: { color: '#ffffff', textDecorationLine: 'underline' as const },\n paragraph: { marginTop: 4, marginBottom: 4, color: '#ffffff' },\n strong: { fontWeight: '700' as const, color: '#ffffff' },\n em: { color: '#ffffff' },\n text: { color: '#ffffff' },\n bullet_list: { marginVertical: 4 },\n ordered_list: { marginVertical: 4 },\n list_item: { marginVertical: 2, color: '#ffffff' },\n};\n\nconst styles = StyleSheet.create({\n list: {\n flex: 1,\n minHeight: 0,\n },\n listContent: {\n paddingTop: 8,\n paddingBottom: 8,\n paddingHorizontal: 8,\n },\n listContentFill: {\n flexGrow: 1,\n },\n userCol: {\n width: '100%',\n alignItems: 'flex-end',\n marginBottom: 12,\n paddingHorizontal: 4,\n },\n userTurn: {\n maxWidth: '92%',\n alignItems: 'flex-end',\n },\n userFollow: {\n marginTop: 4,\n },\n userBubble: {\n backgroundColor: USER_BUBBLE_BG,\n borderRadius: 16,\n paddingHorizontal: 12,\n paddingVertical: 8,\n },\n // Same bubble, held back: a queued turn reads as pending, not as delivered.\n userBubbleQueued: {\n opacity: 0.55,\n },\n queuedCaption: {\n fontSize: 11,\n fontWeight: '500',\n marginTop: 4,\n marginRight: 2,\n },\n assistantGroup: {\n width: '100%',\n marginBottom: 4,\n },\n assistantCol: {\n width: '100%',\n flexGrow: 0,\n alignItems: 'stretch',\n marginBottom: 8,\n paddingHorizontal: 4,\n },\n assistantColTight: {\n marginTop: -4,\n },\n});\n"],"names":["rows","TranscriptGroupView","AssistantTurnView"],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ function stripSurfaceNotes(text) {
2
+ if (!text || !text.includes("(preview `") && !text.includes("(snapshot `")) return text;
3
+ return text.replace(/(restore point) \(snapshot `[^`]+`\)/gi, "$1").replace(/\s*\(preview `[^`]+`\)/gi, "");
4
+ }export{stripSurfaceNotes};//# sourceMappingURL=surfaceNotes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"surfaceNotes.js","sources":["../../../src/features/chat/surfaceNotes.ts"],"sourcesContent":["/**\n * Markers a surface skill writes for its surface, not for the reader (web Web Builder\n * `assistantBubbleContent`, design-pro 3.11): the sandbox preview marker\n * (preview `https://3000-<id>.e2b.app`) - a link that dies with the sandbox; the site\n * opens in Web Builder, which wakes it - and the restore point's id. Pure.\n */\nexport function stripSurfaceNotes(text: string): string {\n if (!text || (!text.includes('(preview `') && !text.includes('(snapshot `'))) return text;\n return text.replace(/(restore point) \\(snapshot `[^`]+`\\)/gi, '$1').replace(/\\s*\\(preview `[^`]+`\\)/gi, '');\n}\n"],"names":[],"mappings":"AAMO,SAAS,kBAAkB,IAAsB,EAAA;AACtD,EAAA,IAAI,CAAC,IAAA,IAAQ,CAAC,IAAA,CAAK,QAAS,CAAA,YAAY,CAAK,IAAA,CAAC,IAAK,CAAA,QAAA,CAAS,aAAa,CAAA,EAAU,OAAA,IAAA;AACnF,EAAA,OAAO,KAAK,OAAQ,CAAA,wCAAA,EAA0C,IAAI,CAAE,CAAA,OAAA,CAAQ,4BAA4B,EAAE,CAAA;AAC5G"}
@@ -1 +1 @@
1
- {"version":3,"file":"backgroundRecording.js","sources":["../../../src/features/dictate/backgroundRecording.ts"],"sourcesContent":["/**\n * The app's background-recording module (portable-devices/mobile/modules/\n * yantra-background-recording), which keeps a dictation going when Yantra\n * leaves the screen, the way a call does:\n * - iOS: the binary declares the `audio` background mode; iOS shows its own\n * recording indicator. The module also reports interruptions (a phone call,\n * another app recording), which expo-audio does not.\n * - Android: a microphone foreground service with an ongoing notification,\n * without which Android gives a backgrounded app only silence.\n *\n * Optional on purpose: an over-the-air JS update can reach a binary built\n * before the module existed. That reads as \"not supported\", and Dictate\n * pauses when it leaves the screen, as before.\n */\nimport { requireOptionalNativeModule } from 'expo-modules-core';\n\nexport type InterruptionEvent = { state: 'began' } | { state: 'ended'; shouldResume: boolean };\n\nexport interface BackgroundRecordingNative {\n /** The binary may keep recording in the background. */\n readonly supported: boolean;\n /** Android starts the microphone service (title and text show in its notification); iOS has nothing to start. */\n start(title: string, text: string): Promise<void>;\n stop(): Promise<void>;\n addListener(event: 'onInterruption', listener: (event: InterruptionEvent) => void): { remove(): void };\n}\n\nlet cached: BackgroundRecordingNative | null | undefined;\n\nexport function backgroundRecordingNative(): BackgroundRecordingNative | null {\n if (cached === undefined) {\n try {\n cached = requireOptionalNativeModule<BackgroundRecordingNative>('YantraBackgroundRecording');\n } catch {\n cached = null;\n }\n }\n return cached;\n}\n\n/** What the Android notification says while a dictation records in the background. */\nexport const BACKGROUND_NOTICE = { title: 'Dictating', text: 'Yantra is recording your dictation. Tap to return.' };\n"],"names":[],"mappings":"4DA+BA,IAAI,MAAA;AACG,SAAS,yBAA8D,GAAA;AAC5E,EAAA,IAAI,WAAW,MAAW,EAAA;AACxB,IAAI,IAAA;AACF,MAAA,MAAA,GAAS,4BAAuD,2BAA2B,CAAA;AAAA,KACrF,CAAA,OAAA,CAAA,EAAA;AACN,MAAS,MAAA,GAAA,IAAA;AAAA;AACX;AAEF,EAAO,OAAA,MAAA;AACT;AAGO,MAAM,iBAAoB,GAAA;AAAA,EAC/B,KAAO,EAAA,WAAA;AAAA,EACP,IAAM,EAAA;AACR"}
1
+ {"version":3,"file":"backgroundRecording.js","sources":["../../../src/features/dictate/backgroundRecording.ts"],"sourcesContent":["/**\n * The app's background-recording module (portable-devices/mobile/modules/\n * yantra-background-recording), which keeps a dictation going when Yantra\n * leaves the screen, the way a call does:\n * - iOS: the binary declares the `audio` background mode, and the recording\n * shows as a Live Activity on the lock screen and in the Dynamic Island. The\n * module also reports interruptions (a phone call, another app recording),\n * which expo-audio does not.\n * - Android: a microphone foreground service with an ongoing notification,\n * without which Android gives a backgrounded app only silence.\n *\n * Optional on purpose: an over-the-air JS update can reach a binary built\n * before the module existed. That reads as \"not supported\", and Dictate\n * pauses when it leaves the screen, as before.\n */\nimport { requireOptionalNativeModule } from 'expo-modules-core';\n\nexport type InterruptionEvent = { state: 'began' } | { state: 'ended'; shouldResume: boolean };\n\n/** What the Live Activity shows, sent on every change. */\nexport interface BackgroundActivityState {\n title: string;\n detail: string;\n recording: boolean;\n /** The recorder's own time, which stops while paused. */\n recordedMs: number;\n noSound: boolean;\n}\n\nexport interface BackgroundRecordingNative {\n /** The binary may keep recording in the background. */\n readonly supported: boolean;\n /** Android starts the microphone service, iOS the Live Activity; both show the title and text. */\n start(title: string, text: string): Promise<void>;\n /** iOS: the Live Activity follows the recording. Missing on Android and on binaries built before it. */\n update?(state: BackgroundActivityState): Promise<void>;\n stop(): Promise<void>;\n addListener(event: 'onInterruption', listener: (event: InterruptionEvent) => void): { remove(): void };\n}\n\nlet cached: BackgroundRecordingNative | null | undefined;\n\nexport function backgroundRecordingNative(): BackgroundRecordingNative | null {\n if (cached === undefined) {\n try {\n cached = requireOptionalNativeModule<BackgroundRecordingNative>('YantraBackgroundRecording');\n } catch {\n cached = null;\n }\n }\n return cached;\n}\n\n/** What the notification (Android) or the Live Activity (iOS) says while a dictation records in the background. */\nexport const BACKGROUND_NOTICE = { title: 'Dictating', text: 'Yantra is recording your dictation. Tap to return.' };\n"],"names":[],"mappings":"4DA4CA,IAAI,MAAA;AACG,SAAS,yBAA8D,GAAA;AAC5E,EAAA,IAAI,WAAW,MAAW,EAAA;AACxB,IAAI,IAAA;AACF,MAAA,MAAA,GAAS,4BAAuD,2BAA2B,CAAA;AAAA,KACrF,CAAA,OAAA,CAAA,EAAA;AACN,MAAS,MAAA,GAAA,IAAA;AAAA;AACX;AAEF,EAAO,OAAA,MAAA;AACT;AAGO,MAAM,iBAAoB,GAAA;AAAA,EAC/B,KAAO,EAAA,WAAA;AAAA,EACP,IAAM,EAAA;AACR"}
@@ -43,6 +43,34 @@ const NO_INPUT_WARN_MS = 4e3;
43
43
  function carriesSignal(db) {
44
44
  return typeof db !== "number" || !Number.isFinite(db) || db > NO_SIGNAL_DB;
45
45
  }
46
+ function backgroundActivityText(phase, pauseReason, noInput) {
47
+ if (phase === "idle") return null;
48
+ if (phase === "recording") {
49
+ return noInput ? {
50
+ title: BACKGROUND_NOTICE.title,
51
+ detail: "No sound is reaching the microphone. Open Yantra to check."
52
+ } : {
53
+ title: BACKGROUND_NOTICE.title,
54
+ detail: BACKGROUND_NOTICE.text
55
+ };
56
+ }
57
+ if (pauseReason === "interrupted") {
58
+ return {
59
+ title: "Paused for a call",
60
+ detail: "A call or another app took the microphone. Open Yantra to resume."
61
+ };
62
+ }
63
+ if (pauseReason === "limit") {
64
+ return {
65
+ title: "Paused at one hour",
66
+ detail: "An hour is the most one dictation holds. Open Yantra to write the note."
67
+ };
68
+ }
69
+ return {
70
+ title: "Paused",
71
+ detail: "Open Yantra to resume or finish the dictation."
72
+ };
73
+ }
46
74
  class MicrophoneDeniedError extends Error {
47
75
  constructor() {
48
76
  super("Microphone access was declined. Allow it in Settings to dictate.");
@@ -135,6 +163,19 @@ function useDictationRecorder() {
135
163
  }
136
164
  }, [phase, state.durationMillis, state.metering]);
137
165
  const noInput = phase === "recording" && !carriesSignal(state.metering) && state.durationMillis - signalAtRef.current >= NO_INPUT_WARN_MS;
166
+ const activity = backgroundActivityText(phase, pauseReason, noInput);
167
+ const activityTitle = activity == null ? void 0 : activity.title;
168
+ const activityDetail = activity == null ? void 0 : activity.detail;
169
+ useEffect(() => {
170
+ if (!activityTitle || !activityDetail || !inBackgroundRef.current || !(background == null ? void 0 : background.update)) return;
171
+ void background.update({
172
+ title: activityTitle,
173
+ detail: activityDetail,
174
+ recording: phase === "recording",
175
+ recordedMs: recorder.getStatus().durationMillis,
176
+ noSound: noInput
177
+ }).catch(() => void 0);
178
+ }, [activityTitle, activityDetail, phase, noInput, background, recorder]);
138
179
  useEffect(() => () => {
139
180
  if (phaseRef.current !== "idle") void recorder.stop().catch(() => void 0);
140
181
  void releaseSession();
@@ -152,4 +193,4 @@ function useDictationRecorder() {
152
193
  resume,
153
194
  finish
154
195
  };
155
- }export{DICTATION_RECORDING,MAX_DICTATION_MS,MicrophoneDeniedError,NO_INPUT_WARN_MS,carriesSignal,levelFromMetering,useDictationRecorder};//# sourceMappingURL=useDictationRecorder.js.map
196
+ }export{DICTATION_RECORDING,MAX_DICTATION_MS,MicrophoneDeniedError,NO_INPUT_WARN_MS,backgroundActivityText,carriesSignal,levelFromMetering,useDictationRecorder};//# sourceMappingURL=useDictationRecorder.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useDictationRecorder.js","sources":["../../../src/features/dictate/useDictationRecorder.ts"],"sourcesContent":["/**\n * The dictation recorder: mono AAC at 64 kbps (speech, about 0.5 MB a minute),\n * pause and resume, and a live level.\n *\n * A binary with the background-recording module keeps recording when Yantra\n * leaves the screen or the phone locks, like a call (backgroundRecording.ts).\n * An older binary cannot: iOS stops the microphone there, so the recorder\n * keeps the screen awake, pauses itself when the app leaves the screen, and\n * says so. Either way, a call or another app taking the microphone pauses the\n * dictation and says why, instead of recording nothing under a running timer.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { AppState } from 'react-native';\nimport {\n AudioQuality,\n IOSOutputFormat,\n RecordingPresets,\n requestRecordingPermissionsAsync,\n setAudioModeAsync,\n setIsAudioActiveAsync,\n useAudioRecorder,\n useAudioRecorderState,\n type RecordingOptions,\n} from 'expo-audio';\nimport { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake';\nimport { BACKGROUND_NOTICE, backgroundRecordingNative } from './backgroundRecording';\n\nexport const DICTATION_RECORDING: RecordingOptions = {\n ...RecordingPresets.HIGH_QUALITY,\n extension: '.m4a',\n sampleRate: 44100,\n numberOfChannels: 1,\n bitRate: 64000,\n isMeteringEnabled: true,\n ios: {\n ...RecordingPresets.HIGH_QUALITY.ios,\n outputFormat: IOSOutputFormat.MPEG4AAC,\n audioQuality: AudioQuality.HIGH,\n },\n android: {\n ...RecordingPresets.HIGH_QUALITY.android,\n outputFormat: 'mpeg4',\n audioEncoder: 'aac',\n },\n};\n\n/** One visit is well under this; past it the recording pauses and asks to be finished. */\nexport const MAX_DICTATION_MS = 60 * 60_000;\nconst KEEP_AWAKE_TAG = 'yantra-dictate';\n\nexport type RecorderPhase = 'idle' | 'recording' | 'paused';\nexport type PauseReason = 'user' | 'background' | 'interrupted' | 'limit' | null;\n\n/** -60 dB (silence) .. 0 dB (loud) mapped to 0..1. */\nexport function levelFromMetering(db: number | undefined): number {\n if (typeof db !== 'number' || !Number.isFinite(db)) return 0;\n return Math.min(1, Math.max(0, (db + 60) / 60));\n}\n\n/**\n * Below this the microphone carries no signal at all (iOS reports -120 to\n * -160 dB): it is blocked, taken by another app, or failed to start. A quiet\n * room still reads about -50 dB, so silence between sentences never trips it.\n */\nconst NO_SIGNAL_DB = -100;\n/** How long a running recording may carry no signal before the clinician is told. */\nexport const NO_INPUT_WARN_MS = 4000;\n\n/** False only for a metering reading that shows no signal; no reading at all says nothing either way. */\nexport function carriesSignal(db: number | undefined): boolean {\n return typeof db !== 'number' || !Number.isFinite(db) || db > NO_SIGNAL_DB;\n}\n\nexport class MicrophoneDeniedError extends Error {\n constructor() {\n super('Microphone access was declined. Allow it in Settings to dictate.');\n this.name = 'MicrophoneDeniedError';\n }\n}\n\nexport function useDictationRecorder() {\n const recorder = useAudioRecorder(DICTATION_RECORDING);\n const state = useAudioRecorderState(recorder, 100);\n const [phase, setPhase] = useState<RecorderPhase>('idle');\n const [pauseReason, setPauseReason] = useState<PauseReason>(null);\n const phaseRef = useRef<RecorderPhase>('idle');\n phaseRef.current = phase;\n const background = useMemo(() => backgroundRecordingNative(), []);\n /** This dictation keeps recording when Yantra leaves the screen. */\n const inBackgroundRef = useRef(false);\n\n const releaseSession = useCallback(async () => {\n if (inBackgroundRef.current) await background?.stop().catch(() => undefined);\n inBackgroundRef.current = false;\n await deactivateKeepAwake(KEEP_AWAKE_TAG).catch(() => undefined);\n // The rest of the app's audio (chat voice notes) does not play on in the background.\n await setAudioModeAsync({\n allowsRecording: false,\n playsInSilentMode: true,\n shouldPlayInBackground: false,\n }).catch(() => undefined);\n }, [background]);\n\n const pause = useCallback(\n (reason: Exclude<PauseReason, null> = 'user') => {\n if (phaseRef.current !== 'recording') return;\n recorder.pause();\n setPhase('paused');\n setPauseReason(reason);\n },\n [recorder],\n );\n\n const start = useCallback(async () => {\n const permission = await requestRecordingPermissionsAsync();\n if (!permission.granted) throw new MicrophoneDeniedError();\n // Android's microphone service has to be running before Yantra leaves the screen, and\n // may only start while it is on it: now. A refusal means pausing on leave, as before.\n inBackgroundRef.current = background?.supported\n ? await background.start(BACKGROUND_NOTICE.title, BACKGROUND_NOTICE.text).then(\n () => true,\n () => false,\n )\n : false;\n try {\n await setAudioModeAsync({\n allowsRecording: true,\n playsInSilentMode: true,\n shouldPlayInBackground: inBackgroundRef.current,\n });\n await recorder.prepareToRecordAsync();\n recorder.record();\n } catch (err) {\n await releaseSession();\n throw err;\n }\n // Without background recording the screen must stay on, or the microphone stops.\n if (!inBackgroundRef.current) await activateKeepAwakeAsync(KEEP_AWAKE_TAG).catch(() => undefined);\n setPhase('recording');\n setPauseReason(null);\n }, [recorder, background, releaseSession]);\n\n const resume = useCallback(async () => {\n if (phaseRef.current !== 'paused') return;\n // A call or another app deactivated the audio session; recording needs it back.\n await setIsAudioActiveAsync(true).catch(() => undefined);\n recorder.record();\n setPhase('recording');\n setPauseReason(null);\n }, [recorder]);\n\n /** Ends the recording; resolves to its file (in the recorder's cache - move it before anything else). */\n const finish = useCallback(async (): Promise<{ uri: string; durationMs: number } | null> => {\n if (phaseRef.current === 'idle') return null;\n const durationMs = recorder.getStatus().durationMillis;\n await recorder.stop();\n setPhase('idle');\n setPauseReason(null);\n await releaseSession();\n return recorder.uri ? { uri: recorder.uri, durationMs } : null;\n }, [recorder, releaseSession]);\n\n // Without background recording the microphone stops when the app leaves the screen: pause, and say so.\n useEffect(() => {\n const sub = AppState.addEventListener('change', (next) => {\n if (next !== 'active' && !inBackgroundRef.current) pause('background');\n });\n return () => sub.remove();\n }, [pause]);\n\n // A call or another app took the microphone. Nothing is recorded from here, though\n // expo-audio's timer would keep running: pause, and stay paused until the clinician resumes.\n useEffect(() => {\n const sub = background?.addListener('onInterruption', (event) => {\n if (event.state === 'began') pause('interrupted');\n });\n return () => sub?.remove();\n }, [background, pause]);\n\n useEffect(() => {\n if (phase === 'recording' && state.durationMillis >= MAX_DICTATION_MS) pause('limit');\n }, [phase, state.durationMillis, pause]);\n\n // Recording time at which the microphone last carried a signal. A paused or idle recorder\n // starts the count again, so Resume gets a fresh few seconds; so does a new recording,\n // whose time restarts below the last one's.\n const signalAtRef = useRef(0);\n useEffect(() => {\n if (phase !== 'recording' || carriesSignal(state.metering) || state.durationMillis < signalAtRef.current) {\n signalAtRef.current = state.durationMillis;\n }\n }, [phase, state.durationMillis, state.metering]);\n // Without this a dead microphone records a whole visit of silence, timer running.\n const noInput =\n phase === 'recording' &&\n !carriesSignal(state.metering) &&\n state.durationMillis - signalAtRef.current >= NO_INPUT_WARN_MS;\n\n // Leaving the screen mid-dictation stops the microphone and lets the screen sleep.\n useEffect(\n () => () => {\n if (phaseRef.current !== 'idle') void recorder.stop().catch(() => undefined);\n void releaseSession();\n },\n [recorder, releaseSession],\n );\n\n return {\n phase,\n pauseReason,\n elapsedMs: state.durationMillis,\n level: phase === 'recording' ? levelFromMetering(state.metering) : 0,\n noInput,\n /** This binary keeps a dictation recording with the phone locked or another app open. */\n recordsInBackground: background?.supported === true,\n start,\n pause: () => pause('user'),\n resume,\n finish,\n };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useDictationRecorder.js","sources":["../../../src/features/dictate/useDictationRecorder.ts"],"sourcesContent":["/**\n * The dictation recorder: mono AAC at 64 kbps (speech, about 0.5 MB a minute),\n * pause and resume, and a live level.\n *\n * A binary with the background-recording module keeps recording when Yantra\n * leaves the screen or the phone locks, like a call, and on iOS shows it on the\n * lock screen and in the Dynamic Island meanwhile (backgroundRecording.ts).\n * An older binary cannot: iOS stops the microphone there, so the recorder\n * keeps the screen awake, pauses itself when the app leaves the screen, and\n * says so. Either way, a call or another app taking the microphone pauses the\n * dictation and says why, instead of recording nothing under a running timer.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { AppState } from 'react-native';\nimport {\n AudioQuality,\n IOSOutputFormat,\n RecordingPresets,\n requestRecordingPermissionsAsync,\n setAudioModeAsync,\n setIsAudioActiveAsync,\n useAudioRecorder,\n useAudioRecorderState,\n type RecordingOptions,\n} from 'expo-audio';\nimport { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake';\nimport { BACKGROUND_NOTICE, backgroundRecordingNative } from './backgroundRecording';\n\nexport const DICTATION_RECORDING: RecordingOptions = {\n ...RecordingPresets.HIGH_QUALITY,\n extension: '.m4a',\n sampleRate: 44100,\n numberOfChannels: 1,\n bitRate: 64000,\n isMeteringEnabled: true,\n ios: {\n ...RecordingPresets.HIGH_QUALITY.ios,\n outputFormat: IOSOutputFormat.MPEG4AAC,\n audioQuality: AudioQuality.HIGH,\n },\n android: {\n ...RecordingPresets.HIGH_QUALITY.android,\n outputFormat: 'mpeg4',\n audioEncoder: 'aac',\n },\n};\n\n/** One visit is well under this; past it the recording pauses and asks to be finished. */\nexport const MAX_DICTATION_MS = 60 * 60_000;\nconst KEEP_AWAKE_TAG = 'yantra-dictate';\n\nexport type RecorderPhase = 'idle' | 'recording' | 'paused';\nexport type PauseReason = 'user' | 'background' | 'interrupted' | 'limit' | null;\n\n/** -60 dB (silence) .. 0 dB (loud) mapped to 0..1. */\nexport function levelFromMetering(db: number | undefined): number {\n if (typeof db !== 'number' || !Number.isFinite(db)) return 0;\n return Math.min(1, Math.max(0, (db + 60) / 60));\n}\n\n/**\n * Below this the microphone carries no signal at all (iOS reports -120 to\n * -160 dB): it is blocked, taken by another app, or failed to start. A quiet\n * room still reads about -50 dB, so silence between sentences never trips it.\n */\nconst NO_SIGNAL_DB = -100;\n/** How long a running recording may carry no signal before the clinician is told. */\nexport const NO_INPUT_WARN_MS = 4000;\n\n/** False only for a metering reading that shows no signal; no reading at all says nothing either way. */\nexport function carriesSignal(db: number | undefined): boolean {\n return typeof db !== 'number' || !Number.isFinite(db) || db > NO_SIGNAL_DB;\n}\n\n/** The Live Activity's words for where the dictation stands; nothing once it is done. */\nexport function backgroundActivityText(\n phase: RecorderPhase,\n pauseReason: PauseReason,\n noInput: boolean,\n): { title: string; detail: string } | null {\n if (phase === 'idle') return null;\n if (phase === 'recording') {\n return noInput\n ? { title: BACKGROUND_NOTICE.title, detail: 'No sound is reaching the microphone. Open Yantra to check.' }\n : { title: BACKGROUND_NOTICE.title, detail: BACKGROUND_NOTICE.text };\n }\n if (pauseReason === 'interrupted') {\n return {\n title: 'Paused for a call',\n detail: 'A call or another app took the microphone. Open Yantra to resume.',\n };\n }\n if (pauseReason === 'limit') {\n return {\n title: 'Paused at one hour',\n detail: 'An hour is the most one dictation holds. Open Yantra to write the note.',\n };\n }\n return { title: 'Paused', detail: 'Open Yantra to resume or finish the dictation.' };\n}\n\nexport class MicrophoneDeniedError extends Error {\n constructor() {\n super('Microphone access was declined. Allow it in Settings to dictate.');\n this.name = 'MicrophoneDeniedError';\n }\n}\n\nexport function useDictationRecorder() {\n const recorder = useAudioRecorder(DICTATION_RECORDING);\n const state = useAudioRecorderState(recorder, 100);\n const [phase, setPhase] = useState<RecorderPhase>('idle');\n const [pauseReason, setPauseReason] = useState<PauseReason>(null);\n const phaseRef = useRef<RecorderPhase>('idle');\n phaseRef.current = phase;\n const background = useMemo(() => backgroundRecordingNative(), []);\n /** This dictation keeps recording when Yantra leaves the screen. */\n const inBackgroundRef = useRef(false);\n\n const releaseSession = useCallback(async () => {\n if (inBackgroundRef.current) await background?.stop().catch(() => undefined);\n inBackgroundRef.current = false;\n await deactivateKeepAwake(KEEP_AWAKE_TAG).catch(() => undefined);\n // The rest of the app's audio (chat voice notes) does not play on in the background.\n await setAudioModeAsync({\n allowsRecording: false,\n playsInSilentMode: true,\n shouldPlayInBackground: false,\n }).catch(() => undefined);\n }, [background]);\n\n const pause = useCallback(\n (reason: Exclude<PauseReason, null> = 'user') => {\n if (phaseRef.current !== 'recording') return;\n recorder.pause();\n setPhase('paused');\n setPauseReason(reason);\n },\n [recorder],\n );\n\n const start = useCallback(async () => {\n const permission = await requestRecordingPermissionsAsync();\n if (!permission.granted) throw new MicrophoneDeniedError();\n // Android's microphone service has to be running before Yantra leaves the screen, and\n // may only start while it is on it: now. A refusal means pausing on leave, as before.\n inBackgroundRef.current = background?.supported\n ? await background.start(BACKGROUND_NOTICE.title, BACKGROUND_NOTICE.text).then(\n () => true,\n () => false,\n )\n : false;\n try {\n await setAudioModeAsync({\n allowsRecording: true,\n playsInSilentMode: true,\n shouldPlayInBackground: inBackgroundRef.current,\n });\n await recorder.prepareToRecordAsync();\n recorder.record();\n } catch (err) {\n await releaseSession();\n throw err;\n }\n // Without background recording the screen must stay on, or the microphone stops.\n if (!inBackgroundRef.current) await activateKeepAwakeAsync(KEEP_AWAKE_TAG).catch(() => undefined);\n setPhase('recording');\n setPauseReason(null);\n }, [recorder, background, releaseSession]);\n\n const resume = useCallback(async () => {\n if (phaseRef.current !== 'paused') return;\n // A call or another app deactivated the audio session; recording needs it back.\n await setIsAudioActiveAsync(true).catch(() => undefined);\n recorder.record();\n setPhase('recording');\n setPauseReason(null);\n }, [recorder]);\n\n /** Ends the recording; resolves to its file (in the recorder's cache - move it before anything else). */\n const finish = useCallback(async (): Promise<{ uri: string; durationMs: number } | null> => {\n if (phaseRef.current === 'idle') return null;\n const durationMs = recorder.getStatus().durationMillis;\n await recorder.stop();\n setPhase('idle');\n setPauseReason(null);\n await releaseSession();\n return recorder.uri ? { uri: recorder.uri, durationMs } : null;\n }, [recorder, releaseSession]);\n\n // Without background recording the microphone stops when the app leaves the screen: pause, and say so.\n useEffect(() => {\n const sub = AppState.addEventListener('change', (next) => {\n if (next !== 'active' && !inBackgroundRef.current) pause('background');\n });\n return () => sub.remove();\n }, [pause]);\n\n // A call or another app took the microphone. Nothing is recorded from here, though\n // expo-audio's timer would keep running: pause, and stay paused until the clinician resumes.\n useEffect(() => {\n const sub = background?.addListener('onInterruption', (event) => {\n if (event.state === 'began') pause('interrupted');\n });\n return () => sub?.remove();\n }, [background, pause]);\n\n useEffect(() => {\n if (phase === 'recording' && state.durationMillis >= MAX_DICTATION_MS) pause('limit');\n }, [phase, state.durationMillis, pause]);\n\n // Recording time at which the microphone last carried a signal. A paused or idle recorder\n // starts the count again, so Resume gets a fresh few seconds; so does a new recording,\n // whose time restarts below the last one's.\n const signalAtRef = useRef(0);\n useEffect(() => {\n if (phase !== 'recording' || carriesSignal(state.metering) || state.durationMillis < signalAtRef.current) {\n signalAtRef.current = state.durationMillis;\n }\n }, [phase, state.durationMillis, state.metering]);\n // Without this a dead microphone records a whole visit of silence, timer running.\n const noInput =\n phase === 'recording' &&\n !carriesSignal(state.metering) &&\n state.durationMillis - signalAtRef.current >= NO_INPUT_WARN_MS;\n\n // The lock screen and the Dynamic Island follow the dictation while it records in the background (iOS).\n const activity = backgroundActivityText(phase, pauseReason, noInput);\n const activityTitle = activity?.title;\n const activityDetail = activity?.detail;\n useEffect(() => {\n if (!activityTitle || !activityDetail || !inBackgroundRef.current || !background?.update) return;\n void background\n .update({\n title: activityTitle,\n detail: activityDetail,\n recording: phase === 'recording',\n recordedMs: recorder.getStatus().durationMillis,\n noSound: noInput,\n })\n .catch(() => undefined);\n }, [activityTitle, activityDetail, phase, noInput, background, recorder]);\n\n // Leaving the screen mid-dictation stops the microphone and lets the screen sleep.\n useEffect(\n () => () => {\n if (phaseRef.current !== 'idle') void recorder.stop().catch(() => undefined);\n void releaseSession();\n },\n [recorder, releaseSession],\n );\n\n return {\n phase,\n pauseReason,\n elapsedMs: state.durationMillis,\n level: phase === 'recording' ? levelFromMetering(state.metering) : 0,\n noInput,\n /** This binary keeps a dictation recording with the phone locked or another app open. */\n recordsInBackground: background?.supported === true,\n start,\n pause: () => pause('user'),\n resume,\n finish,\n };\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ import {useMemo,useState,useEffect}from'react';import AsyncStorage from'@react-native-async-storage/async-storage';import {preferredTemperatureUnit,parsePlace,reverseGeocodeUrl,parseOpenMeteo,openMeteoUrl}from'./weather.js';const PLACE_KEY = "yantra.home.weather.place.v1";
2
+ const REPORT_KEY = "yantra.home.weather.report.v1";
3
+ const PLACE_TTL_MS = 12 * 60 * 6e4;
4
+ const REPORT_TTL_MS = 20 * 6e4;
5
+ const FETCH_TIMEOUT_MS = 1e4;
6
+ async function readCache(key, maxAgeMs) {
7
+ try {
8
+ const raw = await AsyncStorage.getItem(key);
9
+ const stamped = raw ? JSON.parse(raw) : null;
10
+ return stamped && Date.now() - stamped.at < maxAgeMs ? stamped.value : null;
11
+ } catch (e) {
12
+ return null;
13
+ }
14
+ }
15
+ function writeCache(key, value) {
16
+ AsyncStorage.setItem(key, JSON.stringify({
17
+ at: Date.now(),
18
+ value
19
+ })).catch(() => void 0);
20
+ }
21
+ async function fetchJson(url) {
22
+ const controller = new AbortController();
23
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
24
+ try {
25
+ const response = await fetch(url, {
26
+ signal: controller.signal
27
+ });
28
+ if (!response.ok) throw new Error(`${response.status}`);
29
+ return await response.json();
30
+ } finally {
31
+ clearTimeout(timer);
32
+ }
33
+ }
34
+ function useHomeWeather(enabled = true) {
35
+ const unit = useMemo(() => preferredTemperatureUnit(), []);
36
+ const [place, setPlace] = useState();
37
+ const [report, setReport] = useState();
38
+ useEffect(() => {
39
+ if (!enabled || place) return void 0;
40
+ let cancelled = false;
41
+ (async () => {
42
+ const known = await readCache(PLACE_KEY, PLACE_TTL_MS);
43
+ if (known) {
44
+ if (!cancelled) setPlace(known);
45
+ return;
46
+ }
47
+ const found = parsePlace(await fetchJson(reverseGeocodeUrl()), false);
48
+ if (!found || cancelled) return;
49
+ writeCache(PLACE_KEY, found);
50
+ setPlace(found);
51
+ })().catch(() => void 0);
52
+ return () => {
53
+ cancelled = true;
54
+ };
55
+ }, [enabled, place]);
56
+ useEffect(() => {
57
+ if (!enabled || !place) return void 0;
58
+ let cancelled = false;
59
+ const key = `${place.latitude.toFixed(2)},${place.longitude.toFixed(2)},${unit}`;
60
+ const load = async () => {
61
+ const cached = await readCache(REPORT_KEY, REPORT_TTL_MS - 6e4);
62
+ if ((cached == null ? void 0 : cached.key) === key) {
63
+ if (!cancelled) setReport(cached.report);
64
+ return;
65
+ }
66
+ const next = parseOpenMeteo(await fetchJson(openMeteoUrl(place, unit)), unit);
67
+ if (!next || cancelled) return;
68
+ writeCache(REPORT_KEY, {
69
+ key,
70
+ report: next
71
+ });
72
+ setReport(next);
73
+ };
74
+ load().catch(() => void 0);
75
+ const timer = setInterval(() => load().catch(() => void 0), REPORT_TTL_MS);
76
+ return () => {
77
+ cancelled = true;
78
+ clearInterval(timer);
79
+ };
80
+ }, [enabled, place, unit]);
81
+ return {
82
+ place,
83
+ report
84
+ };
85
+ }export{useHomeWeather};//# sourceMappingURL=useHomeWeather.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useHomeWeather.js","sources":["../../../src/features/home/useHomeWeather.ts"],"sourcesContent":["/**\n * The place and weather for the chat home's ambient line, as the web's useWeather\n * (account/browser features/panels): the approximate place for the phone's network - so nothing\n * is asked - then Open-Meteo's conditions there, re-read every 20 minutes while the home is open.\n * Both are kept on the device (the place for 12 hours, the report for 20 minutes), so a cold\n * start paints them at once. Any failure just leaves them out of the line.\n */\nimport { useEffect, useMemo, useState } from 'react';\nimport AsyncStorage from '@react-native-async-storage/async-storage';\nimport {\n openMeteoUrl,\n parseOpenMeteo,\n parsePlace,\n preferredTemperatureUnit,\n reverseGeocodeUrl,\n type WeatherPlace,\n type WeatherReport,\n} from './weather';\n\nconst PLACE_KEY = 'yantra.home.weather.place.v1';\nconst REPORT_KEY = 'yantra.home.weather.report.v1';\nconst PLACE_TTL_MS = 12 * 60 * 60_000;\nconst REPORT_TTL_MS = 20 * 60_000;\nconst FETCH_TIMEOUT_MS = 10_000;\n\ninterface Stamped<T> {\n at: number;\n value: T;\n}\n\nasync function readCache<T>(key: string, maxAgeMs: number): Promise<T | null> {\n try {\n const raw = await AsyncStorage.getItem(key);\n const stamped = raw ? (JSON.parse(raw) as Stamped<T>) : null;\n return stamped && Date.now() - stamped.at < maxAgeMs ? stamped.value : null;\n } catch {\n return null;\n }\n}\n\nfunction writeCache<T>(key: string, value: T): void {\n AsyncStorage.setItem(key, JSON.stringify({ at: Date.now(), value })).catch(() => undefined);\n}\n\nasync function fetchJson(url: string): Promise<unknown> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);\n try {\n const response = await fetch(url, { signal: controller.signal });\n if (!response.ok) throw new Error(`${response.status}`);\n return await response.json();\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport interface HomeWeather {\n place?: WeatherPlace;\n report?: WeatherReport;\n}\n\nexport function useHomeWeather(enabled = true): HomeWeather {\n const unit = useMemo(() => preferredTemperatureUnit(), []);\n const [place, setPlace] = useState<WeatherPlace>();\n const [report, setReport] = useState<WeatherReport>();\n\n // The place: the one kept on the device, else the approximate one for this network.\n useEffect(() => {\n if (!enabled || place) return undefined;\n let cancelled = false;\n (async () => {\n const known = await readCache<WeatherPlace>(PLACE_KEY, PLACE_TTL_MS);\n if (known) {\n if (!cancelled) setPlace(known);\n return;\n }\n const found = parsePlace(await fetchJson(reverseGeocodeUrl()), false);\n if (!found || cancelled) return;\n writeCache(PLACE_KEY, found);\n setPlace(found);\n })().catch(() => undefined);\n return () => {\n cancelled = true;\n };\n }, [enabled, place]);\n\n // The conditions there, re-read every 20 minutes while the home is open.\n useEffect(() => {\n if (!enabled || !place) return undefined;\n let cancelled = false;\n const key = `${place.latitude.toFixed(2)},${place.longitude.toFixed(2)},${unit}`;\n const load = async () => {\n // A minute short of the interval, so each tick reads fresh rather than its own last answer.\n const cached = await readCache<{ key: string; report: WeatherReport }>(REPORT_KEY, REPORT_TTL_MS - 60_000);\n if (cached?.key === key) {\n if (!cancelled) setReport(cached.report);\n return;\n }\n const next = parseOpenMeteo(await fetchJson(openMeteoUrl(place, unit)), unit);\n if (!next || cancelled) return;\n writeCache(REPORT_KEY, { key, report: next });\n setReport(next);\n };\n load().catch(() => undefined);\n const timer = setInterval(() => load().catch(() => undefined), REPORT_TTL_MS);\n return () => {\n cancelled = true;\n clearInterval(timer);\n };\n }, [enabled, place, unit]);\n\n return { place, report };\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,139 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defProps = Object.defineProperties;
3
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __spreadValues = (a, b) => {
9
+ for (var prop in b || (b = {}))
10
+ if (__hasOwnProp.call(b, prop))
11
+ __defNormalProp(a, prop, b[prop]);
12
+ if (__getOwnPropSymbols)
13
+ for (var prop of __getOwnPropSymbols(b)) {
14
+ if (__propIsEnum.call(b, prop))
15
+ __defNormalProp(a, prop, b[prop]);
16
+ }
17
+ return a;
18
+ };
19
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
20
+ const BIGDATACLOUD_URL = "https://api.bigdatacloud.net/data/reverse-geocode-client";
21
+ const OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast";
22
+ const FAHRENHEIT_REGIONS = /* @__PURE__ */ new Set(["US", "BS", "BZ", "KY", "LR", "PW", "FM", "MH"]);
23
+ function preferredTemperatureUnit(locale) {
24
+ var _a, _b;
25
+ let tag = "";
26
+ if (!tag) {
27
+ try {
28
+ tag = Intl.DateTimeFormat().resolvedOptions().locale;
29
+ } catch (e) {
30
+ tag = "en";
31
+ }
32
+ }
33
+ const region = (_b = (_a = tag.split(/[-_]/).slice(1).find((part) => /^[A-Za-z]{2}$/.test(part))) == null ? void 0 : _a.toUpperCase()) != null ? _b : "";
34
+ return FAHRENHEIT_REGIONS.has(region) ? "fahrenheit" : "celsius";
35
+ }
36
+ function query(params) {
37
+ return Object.entries(params).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
38
+ }
39
+ function reverseGeocodeUrl(coords) {
40
+ return `${BIGDATACLOUD_URL}?${query(__spreadProps(__spreadValues({}, {}), {
41
+ localityLanguage: "en"
42
+ }))}`;
43
+ }
44
+ const finite = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
45
+ const label = (value) => typeof value === "string" ? value.trim() : "";
46
+ function parsePlace(json, precise) {
47
+ const data = json != null ? json : {};
48
+ const latitude = finite(data.latitude);
49
+ const longitude = finite(data.longitude);
50
+ if (latitude === void 0 || longitude === void 0) return null;
51
+ if (Math.abs(latitude) > 90 || Math.abs(longitude) > 180 || latitude === 0 && longitude === 0) return null;
52
+ const town = label(data.locality) || label(data.city);
53
+ const region = label(data.principalSubdivision) || label(data.countryName) || label(data.countryCode);
54
+ const parts = [town, region].filter((part, index, all) => part && all.indexOf(part) === index);
55
+ return {
56
+ latitude,
57
+ longitude,
58
+ label: parts.join(", ") || "Your area",
59
+ precise
60
+ };
61
+ }
62
+ function openMeteoUrl(place, unit) {
63
+ return `${OPEN_METEO_URL}?${query(__spreadValues({
64
+ // Two decimals (~1 km) is all a forecast needs, and keeps the cache key stable.
65
+ latitude: place.latitude.toFixed(2),
66
+ longitude: place.longitude.toFixed(2),
67
+ current: "temperature_2m,weather_code,is_day",
68
+ timezone: "auto",
69
+ forecast_days: "1"
70
+ }, unit === "fahrenheit" ? {
71
+ temperature_unit: "fahrenheit"
72
+ } : {}))}`;
73
+ }
74
+ function parseOpenMeteo(json, unit) {
75
+ var _a;
76
+ const current = (_a = (json != null ? json : {}).current) != null ? _a : {};
77
+ const temperature = finite(current.temperature_2m);
78
+ const code = finite(current.weather_code);
79
+ if (temperature === void 0 || code === void 0) return null;
80
+ return {
81
+ temperature,
82
+ code,
83
+ isDay: current.is_day !== 0,
84
+ unit
85
+ };
86
+ }
87
+ function describeWeather(code, isDay) {
88
+ if (code === 0) return {
89
+ label: isDay ? "Sunny" : "Clear",
90
+ glyph: isDay ? "sun" : "moon"
91
+ };
92
+ if (code === 1) return {
93
+ label: "Mostly clear",
94
+ glyph: isDay ? "sun" : "moon"
95
+ };
96
+ if (code === 2) return {
97
+ label: "Partly cloudy",
98
+ glyph: isDay ? "cloud-sun" : "cloud-moon"
99
+ };
100
+ if (code === 3) return {
101
+ label: "Overcast",
102
+ glyph: "cloud"
103
+ };
104
+ if (code === 45 || code === 48) return {
105
+ label: "Foggy",
106
+ glyph: "fog"
107
+ };
108
+ if (code >= 51 && code <= 57) return {
109
+ label: "Drizzle",
110
+ glyph: "drizzle"
111
+ };
112
+ if (code >= 61 && code <= 67) return {
113
+ label: code >= 65 ? "Heavy rain" : "Rain",
114
+ glyph: "rain"
115
+ };
116
+ if (code >= 71 && code <= 77) return {
117
+ label: "Snow",
118
+ glyph: "snow"
119
+ };
120
+ if (code >= 80 && code <= 82) return {
121
+ label: "Showers",
122
+ glyph: "rain"
123
+ };
124
+ if (code === 85 || code === 86) return {
125
+ label: "Snow showers",
126
+ glyph: "snow"
127
+ };
128
+ if (code >= 95) return {
129
+ label: "Thunderstorms",
130
+ glyph: "storm"
131
+ };
132
+ return {
133
+ label: "Cloudy",
134
+ glyph: "cloud"
135
+ };
136
+ }
137
+ function degrees(value) {
138
+ return value === void 0 ? "\u2013" : `${Math.round(value)}\xB0`;
139
+ }export{BIGDATACLOUD_URL,OPEN_METEO_URL,degrees,describeWeather,openMeteoUrl,parseOpenMeteo,parsePlace,preferredTemperatureUnit,reverseGeocodeUrl};//# sourceMappingURL=weather.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"weather.js","sources":["../../../src/features/home/weather.ts"],"sourcesContent":["/**\n * The chat home's place and weather, as the web's (account/browser features/panels/weather.ts):\n * the place from BigDataCloud (its answer for the caller's network, so nothing is asked) and the\n * conditions from Open-Meteo - two keyless services. Pure helpers only; useHomeWeather fetches\n * and caches.\n *\n * Copied, not imported: mobile shares no code with the web shell. URLs are built by hand, since\n * React Native's `URL` polyfill throws on most of its API.\n */\n\nexport type TemperatureUnit = 'celsius' | 'fahrenheit';\n\nexport type WeatherGlyph =\n | 'sun'\n | 'moon'\n | 'cloud-sun'\n | 'cloud-moon'\n | 'cloud'\n | 'fog'\n | 'drizzle'\n | 'rain'\n | 'snow'\n | 'storm';\n\nexport interface WeatherPlace {\n latitude: number;\n longitude: number;\n /** \"Curchorem, Goa\". */\n label: string;\n /** From the person's own position (true) or their network's approximate one (false). */\n precise: boolean;\n}\n\nexport interface WeatherReport {\n temperature: number;\n code: number;\n isDay: boolean;\n unit: TemperatureUnit;\n}\n\nexport const BIGDATACLOUD_URL = 'https://api.bigdatacloud.net/data/reverse-geocode-client';\nexport const OPEN_METEO_URL = 'https://api.open-meteo.com/v1/forecast';\n\n/** Countries that read temperatures in Fahrenheit. */\nconst FAHRENHEIT_REGIONS = new Set(['US', 'BS', 'BZ', 'KY', 'LR', 'PW', 'FM', 'MH']);\n\n/** The device locale's region decides the unit, as the browser's language does on the web. */\nexport function preferredTemperatureUnit(locale?: string): TemperatureUnit {\n let tag = locale ?? '';\n if (!tag) {\n try {\n tag = Intl.DateTimeFormat().resolvedOptions().locale;\n } catch {\n tag = 'en';\n }\n }\n const region =\n tag\n .split(/[-_]/)\n .slice(1)\n .find((part) => /^[A-Za-z]{2}$/.test(part))\n ?.toUpperCase() ?? '';\n return FAHRENHEIT_REGIONS.has(region) ? 'fahrenheit' : 'celsius';\n}\n\nfunction query(params: Record<string, string>): string {\n return Object.entries(params)\n .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)\n .join('&');\n}\n\n/** With coordinates: reverse geocoding. Without: the service answers for the caller's network (approximate). */\nexport function reverseGeocodeUrl(coords?: { latitude: number; longitude: number }): string {\n return `${BIGDATACLOUD_URL}?${query({\n ...(coords ? { latitude: coords.latitude.toFixed(4), longitude: coords.longitude.toFixed(4) } : {}),\n localityLanguage: 'en',\n })}`;\n}\n\nconst finite = (value: unknown): number | undefined =>\n typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n\nconst label = (value: unknown): string => (typeof value === 'string' ? value.trim() : '');\n\n/** A BigDataCloud answer as a place; null without usable coordinates. */\nexport function parsePlace(json: unknown, precise: boolean): WeatherPlace | null {\n const data = (json ?? {}) as Record<string, unknown>;\n const latitude = finite(data.latitude);\n const longitude = finite(data.longitude);\n if (latitude === undefined || longitude === undefined) return null;\n if (Math.abs(latitude) > 90 || Math.abs(longitude) > 180 || (latitude === 0 && longitude === 0)) return null;\n const town = label(data.locality) || label(data.city);\n const region = label(data.principalSubdivision) || label(data.countryName) || label(data.countryCode);\n const parts = [town, region].filter((part, index, all) => part && all.indexOf(part) === index);\n return { latitude, longitude, label: parts.join(', ') || 'Your area', precise };\n}\n\nexport function openMeteoUrl(place: Pick<WeatherPlace, 'latitude' | 'longitude'>, unit: TemperatureUnit): string {\n return `${OPEN_METEO_URL}?${query({\n // Two decimals (~1 km) is all a forecast needs, and keeps the cache key stable.\n latitude: place.latitude.toFixed(2),\n longitude: place.longitude.toFixed(2),\n current: 'temperature_2m,weather_code,is_day',\n timezone: 'auto',\n forecast_days: '1',\n ...(unit === 'fahrenheit' ? { temperature_unit: 'fahrenheit' } : {}),\n })}`;\n}\n\nexport function parseOpenMeteo(json: unknown, unit: TemperatureUnit): WeatherReport | null {\n const current = ((json ?? {}) as { current?: Record<string, unknown> }).current ?? {};\n const temperature = finite(current.temperature_2m);\n const code = finite(current.weather_code);\n if (temperature === undefined || code === undefined) return null;\n return { temperature, code, isDay: current.is_day !== 0, unit };\n}\n\n/** WMO weather interpretation codes (Open-Meteo's `weather_code`), in the web's words. */\nexport function describeWeather(code: number, isDay: boolean): { label: string; glyph: WeatherGlyph } {\n if (code === 0) return { label: isDay ? 'Sunny' : 'Clear', glyph: isDay ? 'sun' : 'moon' };\n if (code === 1) return { label: 'Mostly clear', glyph: isDay ? 'sun' : 'moon' };\n if (code === 2) return { label: 'Partly cloudy', glyph: isDay ? 'cloud-sun' : 'cloud-moon' };\n if (code === 3) return { label: 'Overcast', glyph: 'cloud' };\n if (code === 45 || code === 48) return { label: 'Foggy', glyph: 'fog' };\n if (code >= 51 && code <= 57) return { label: 'Drizzle', glyph: 'drizzle' };\n if (code >= 61 && code <= 67) return { label: code >= 65 ? 'Heavy rain' : 'Rain', glyph: 'rain' };\n if (code >= 71 && code <= 77) return { label: 'Snow', glyph: 'snow' };\n if (code >= 80 && code <= 82) return { label: 'Showers', glyph: 'rain' };\n if (code === 85 || code === 86) return { label: 'Snow showers', glyph: 'snow' };\n if (code >= 95) return { label: 'Thunderstorms', glyph: 'storm' };\n return { label: 'Cloudy', glyph: 'cloud' };\n}\n\n/** \"25°\" - whole degrees. */\nexport function degrees(value: number | undefined): string {\n return value === undefined ? '–' : `${Math.round(value)}°`;\n}\n"],"names":[],"mappings":"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"}