mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
* build(mobile): upgrade to Expo SDK 58 and React Native 0.88 Expo 58 drops Zod from @expo/cli, which clears the last Zod 3 advisory (GHSA-mrhx-c7xc-m3w5) in mobile along with its local patch and override. - Expo 55 -> 58, React Native 0.83 -> 0.88, React 19.3, gesture-handler 3, webview 14, reanimated 4.7, worklets 0.13 (SDK 58 bundled versions, pinned below today's batch to respect minimumReleaseAge). - Port the react-native and react-native-webview patches unchanged; drop expo-constants, expo-notifications, @expo/cli, expo-router, zod and query-string patches plus the zod/query-string/xcode>uuid overrides, which upstream fixed or no longer need. - iOS minimum rises to 16.4 (Expo 58 / RN 0.88 requirement). - Migrate to RN 0.88 strict types: instance ref types, NamedStyles, AppState.currentState, list header/footer nulls. - Replace removed runtime APIs: InteractionManager -> idle callback with a macrotask fallback; StyleSheet.absoluteFillObject -> absoluteFill; FileHandle.readBytes -> readBytesSync (it became async). - Resolve react-native/asset-registry for shared assets in Metro and alias the old registry path for react-native-svg in the desktop mobile-web bundle. - Reduce the iOS scene plugin to the reconnect override; Expo 58's template now ships the scene delegate itself. * fix(mobile): bridge react-native-svg web imports and libsodium runtime for RN 0.88 - Share react-native-svg web shims between the mobile-web build and its render checks: map the old asset-registry path to react-native-web's registry and stub the Android-only touch-sound TurboModule. - Patch react-native-libsodium 1.7.0 to read the JSI runtime from the bridge proxy; RN 0.88 removed RCTCxxBridge.runtime. * fix(mobile): use Expo 58's legacy promise resolver for audio permissions * fix(mobile): skip null handles in the RPC recording timer shims for RN 0.88 types * style(mobile): format MobilePRSidebar after rebase * fix(mobile): name RN 0.88 text input refs for declarations; allow queued back in stack render check - Type text input refs with an exported interface so declaration emit can name hook results (RN 0.88 aliases TextInputInstance to an unexported class). - Expo Router 58 drains back() after a commit; check the interrupted push's exit from the frame it starts, bounded to three frames of lag. * chore(mobile): justify the text input handle interface for React Doctor * chore(mobile): use a nameable intersection alias for text input refs * fix(mobile): use sync expo-file-system writes, copies and moves on Expo 58 Expo 58 made File.write/copy/move and FileHandle.writeBytes return promises. Our callers never awaited them, so failures escaped their try/catch and the store's tmp-then-move and PDF writes could race their close or rename. * build(mobile): raise the page bundle budget to 10 MiB for Expo 58 Expo 58 adds about 310 KB to the page: expo-router (+258 KB, now carrying React Navigation, net about +110 KB) and Reanimated 4.7 (+152 KB). Main had 85 KB of headroom left. 10 MiB still stays within the 10 MiB per-asset contract. * fix(mobile): drop the window-metrics seed RN 0.88 removed from Android RN 0.88 no longer holds window metrics: DeviceInfoModule reads the activity's WindowMetrics on demand and setWindowDisplayMetrics is gone, so MainActivity no longer compiled. Layout scale still comes from the screen metrics we reseed. --------- Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
163 lines
5.6 KiB
TypeScript
163 lines
5.6 KiB
TypeScript
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
import { Platform, ScrollView, Text, View, type ScrollViewInstance } from 'react-native'
|
|
import type {
|
|
AttachmentTextSource,
|
|
AttachmentTextWindow
|
|
} from '../../../src/shared/attachment-text/attachment-text-source'
|
|
import { AttachmentTextViewport } from '../../../src/shared/attachment-text/attachment-text-viewport'
|
|
import { chatAttachmentPreviewStyles as styles } from './mobile-file-preview-styles'
|
|
|
|
function scrollElement(view: ScrollViewInstance | null): HTMLElement | null {
|
|
if (Platform.OS !== 'web') {
|
|
return null
|
|
}
|
|
const node: unknown =
|
|
view && 'getScrollableNode' in view && typeof view.getScrollableNode === 'function'
|
|
? view.getScrollableNode()
|
|
: view
|
|
return node instanceof HTMLElement ? node : null
|
|
}
|
|
|
|
export function AttachmentTextScrollView({
|
|
source,
|
|
revision,
|
|
demand
|
|
}: {
|
|
source: AttachmentTextSource
|
|
revision: number
|
|
demand: (window: AttachmentTextWindow) => void
|
|
}) {
|
|
const scroll = useRef<ScrollViewInstance>(null)
|
|
const viewport = useRef<ScrollViewInstance>(null)
|
|
const nativeOffset = useRef(0)
|
|
const [position, setPosition] = useState({ height: 0, rowHeight: 19 })
|
|
const measured = useMemo(
|
|
() => new AttachmentTextViewport(source, position.rowHeight, position.height, revision),
|
|
[source, revision, position.rowHeight, position.height]
|
|
)
|
|
const [projection, setProjection] = useState(() => ({ layout: measured, top: 0 }))
|
|
const { layout } = projection
|
|
// Snapshot the live old track before publishing an extent that could clamp an undelivered gesture.
|
|
useLayoutEffect(() => {
|
|
if (layout !== measured) {
|
|
const live = scrollElement(scroll.current)?.scrollTop ?? nativeOffset.current
|
|
setProjection({ layout: measured, top: measured.restore(layout, live) })
|
|
}
|
|
}, [measured, layout, projection.top])
|
|
useLayoutEffect(() => {
|
|
const live = scrollElement(scroll.current)?.scrollTop ?? nativeOffset.current
|
|
if (Math.abs(live - projection.top) / layout.scale > 0.01) {
|
|
scroll.current?.scrollTo({ y: projection.top, animated: false })
|
|
nativeOffset.current = projection.top
|
|
}
|
|
const actual = scrollElement(scroll.current)?.scrollTop ?? nativeOffset.current
|
|
setProjection((old) => (old.top === actual ? old : { ...old, top: actual }))
|
|
}, [layout])
|
|
const window = layout.window(projection.top)
|
|
useEffect(() => {
|
|
if (source.size !== null) {
|
|
demand(window)
|
|
}
|
|
}, [source, demand, window.block, window.row, window.rows, window.fraction, window.tail])
|
|
const model = useRef(layout)
|
|
useLayoutEffect(() => {
|
|
model.current = layout
|
|
})
|
|
useEffect(() => {
|
|
const node = scrollElement(scroll.current)
|
|
if (!node) {
|
|
return
|
|
}
|
|
const move = (pixels: number) => {
|
|
node.scrollTop = model.current.move(model.current.anchor(node.scrollTop), pixels)
|
|
}
|
|
const wheel = (event: WheelEvent) => {
|
|
if (event.ctrlKey || Math.abs(event.deltaX) > Math.abs(event.deltaY)) {
|
|
return
|
|
}
|
|
event.preventDefault()
|
|
move(
|
|
event.deltaY *
|
|
(event.deltaMode === 1
|
|
? model.current.rowHeight
|
|
: event.deltaMode === 2
|
|
? node.clientHeight
|
|
: 1)
|
|
)
|
|
}
|
|
node.addEventListener('wheel', wheel, { passive: false })
|
|
return () => {
|
|
node.removeEventListener('wheel', wheel)
|
|
}
|
|
}, [])
|
|
const [width, setWidth] = useState(0)
|
|
const [cellWidth, setCellWidth] = useState(8)
|
|
const rows = layout.rows(projection.top)
|
|
const contentWidth = Math.max(width, source.displayUnits * cellWidth + 32)
|
|
return (
|
|
<ScrollView
|
|
ref={viewport}
|
|
horizontal
|
|
style={styles.scroll}
|
|
contentContainerStyle={{ flexGrow: 1 }}
|
|
onLayout={(event) => {
|
|
setWidth(event.nativeEvent.layout.width)
|
|
const height =
|
|
scrollElement(viewport.current)?.clientHeight ?? event.nativeEvent.layout.height
|
|
setPosition((old) => ({ ...old, height }))
|
|
}}
|
|
>
|
|
<ScrollView
|
|
ref={scroll}
|
|
style={{ width: contentWidth, height: layout.height, flexGrow: 0, flexShrink: 0 }}
|
|
scrollEventThrottle={16}
|
|
onScroll={(event) => {
|
|
const top = event.nativeEvent.contentOffset.y
|
|
nativeOffset.current = top
|
|
setProjection((old) => ({ ...old, top }))
|
|
}}
|
|
>
|
|
<View style={{ height: layout.total, position: 'relative', overflow: 'hidden' }}>
|
|
<Text
|
|
accessible={false}
|
|
style={[
|
|
styles.textPreview,
|
|
{ position: 'absolute', opacity: 0, alignSelf: 'flex-start' }
|
|
]}
|
|
onTextLayout={(event) => {
|
|
const line = event.nativeEvent.lines[0]
|
|
if (line?.height) {
|
|
setPosition((old) => ({ ...old, rowHeight: line.height }))
|
|
setCellWidth(line.width)
|
|
}
|
|
}}
|
|
>
|
|
M
|
|
</Text>
|
|
{rows.map((row) => (
|
|
<Text
|
|
key={row.key}
|
|
testID="attachment-text-segment"
|
|
selectable
|
|
numberOfLines={1}
|
|
style={[
|
|
styles.textPreview,
|
|
{
|
|
...(Platform.OS === 'web' ? { whiteSpace: 'pre' as const } : {}),
|
|
position: 'absolute',
|
|
top: row.top,
|
|
height: layout.rowHeight,
|
|
left: 16,
|
|
width: contentWidth - 32
|
|
}
|
|
]}
|
|
>
|
|
{row.text.replace(/\n$/, '') || '\u00a0'}
|
|
</Text>
|
|
))}
|
|
</View>
|
|
</ScrollView>
|
|
</ScrollView>
|
|
)
|
|
}
|