diff --git a/js/app/utils/facet.ts b/js/app/utils/facet.ts deleted file mode 100644 index 544bebdc..00000000 --- a/js/app/utils/facet.ts +++ /dev/null @@ -1,131 +0,0 @@ -// Taken from ATcute's richtext-segmenter -// https://github.com/mary-ext/atcute/blob/trunk/packages/bluesky/richtext-segmenter/lib/index.ts -// repoed b/c we need to import types from @atproto/api not @atcute/bsky -import { Main } from "@atproto/api/dist/client/types/app/bsky/richtext/facet"; - -type UnwrapArray = T extends (infer V)[] ? V : never; - -export type Facet = Main; -export type FacetFeature = UnwrapArray; - -export interface RichtextSegment { - text: string; - features: FacetFeature[] | undefined; -} - -const segment = ( - text: string, - features: FacetFeature[] | undefined, -): RichtextSegment => { - return { text, features: text.length > 0 ? features : undefined }; -}; - -export const segmentize = ( - text: string, - facets: Facet[] | undefined, -): RichtextSegment[] => { - if (facets === undefined || facets.length === 0) { - return [segment(text, undefined)]; - } - - const segments: RichtextSegment[] = []; - const utf16Length = text.length; - let utf16Cursor = 0; - let utf8Cursor = 0; - - const advanceCursor = (startUtf16: number, endUtf8: number): number => { - let curs = startUtf16; - - // Fast-path for entirely ASCII text - const isLikelyAsciiText = text.charCodeAt(curs) < 0x80; - if (isLikelyAsciiText) { - curs += 1; - utf8Cursor += 1; - - // SIMD-like batch processing - while (utf8Cursor + 8 <= endUtf8 && curs + 8 <= utf16Length) { - const char1 = text.charCodeAt(curs); - const char2 = text.charCodeAt(curs + 1); - const char3 = text.charCodeAt(curs + 2); - const char4 = text.charCodeAt(curs + 3); - const char5 = text.charCodeAt(curs + 4); - const char6 = text.charCodeAt(curs + 5); - const char7 = text.charCodeAt(curs + 6); - const char8 = text.charCodeAt(curs + 7); - - if ( - (char1 | char2 | char3 | char4 | char5 | char6 | char7 | char8) < - 0x80 - ) { - curs += 8; - utf8Cursor += 8; - continue; - } - - break; - } - } - - // Process remaining characters individually - while (utf8Cursor < endUtf8 && curs < utf16Length) { - const code = text.charCodeAt(curs); - - if (code < 0x80) { - curs += 1; - utf8Cursor += 1; - } else if (code < 0x800) { - curs += 1; - utf8Cursor += 2; - } else if (code < 0xd800 || code > 0xdbff) { - curs += 1; - utf8Cursor += 3; - } else { - curs += 2; - utf8Cursor += 4; - } - } - - return curs; - }; - - // Process facets - for (let idx = 0, len = facets.length; idx < len; idx++) { - const facet = facets[idx]; - - const { byteStart, byteEnd } = facet.index; - const features = facet.features; - - if (byteStart > byteEnd || features.length === 0) { - continue; - } - - if (utf8Cursor < byteStart) { - const nextUtf16Cursor = advanceCursor(utf16Cursor, byteStart); - if (nextUtf16Cursor > utf16Cursor) { - segments.push( - segment(text.slice(utf16Cursor, nextUtf16Cursor), undefined), - ); - } - - utf16Cursor = nextUtf16Cursor; - } - - { - const nextUtf16Cursor = advanceCursor(utf16Cursor, byteEnd); - if (nextUtf16Cursor > utf16Cursor) { - segments.push( - segment(text.slice(utf16Cursor, nextUtf16Cursor), features), - ); - } - - utf16Cursor = nextUtf16Cursor; - } - } - - // Handle remaining text - if (utf16Cursor < utf16Length) { - segments.push(segment(text.slice(utf16Cursor), undefined)); - } - - return segments; -}; diff --git a/js/components/src/components/chat/chat.tsx b/js/components/src/components/chat/chat.tsx index 844abc02..205c6300 100644 --- a/js/components/src/components/chat/chat.tsx +++ b/js/components/src/components/chat/chat.tsx @@ -13,6 +13,7 @@ import Reanimated, { } from "react-native-reanimated"; import { ChatMessageViewHydrated } from "streamplace"; import { + ErrorBoundary, getSystemMessageType, SystemMessage, SystemMessageType, @@ -321,7 +322,11 @@ export function Chat({ data={chat.slice(0, shownMessages)} inverted={true} keyExtractor={keyExtractor} - renderItem={({ item, index }) => } + renderItem={({ item, index }) => ( + + + + )} removeClippedSubviews={true} maxToRenderPerBatch={10} initialNumToRender={10} diff --git a/js/components/src/components/error-boundary.tsx b/js/components/src/components/error-boundary.tsx new file mode 100644 index 00000000..95f808f3 --- /dev/null +++ b/js/components/src/components/error-boundary.tsx @@ -0,0 +1,42 @@ +import * as React from "react"; +import { Text, View } from "react-native"; + +type ErrorBoundaryProps = { + children: React.ReactNode; +}; + +type ErrorBoundaryState = { + hasError: boolean; +}; + +export class ErrorBoundary extends React.Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: any): ErrorBoundaryState { + // Update state so the next render will show the fallback UI. + return { hasError: true }; + } + + componentDidCatch(error: any, info: any) { + console.error(" caught:", error); + } + + render() { + if (this.state.hasError) { + // You can render any custom fallback UI + return ( + + Error + + ); + } + + return this.props.children; + } +} diff --git a/js/components/src/index.tsx b/js/components/src/index.tsx index 238476ae..4e7b9fc3 100644 --- a/js/components/src/index.tsx +++ b/js/components/src/index.tsx @@ -7,6 +7,8 @@ export * from "./player-store"; export * from "./streamplace-provider"; export * from "./streamplace-store"; +export { ErrorBoundary } from "./components/error-boundary"; + export { PlayerProvider, withPlayerProvider, diff --git a/js/components/src/lib/facet.ts b/js/components/src/lib/facet.ts index 544bebdc..69522686 100644 --- a/js/components/src/lib/facet.ts +++ b/js/components/src/lib/facet.ts @@ -95,7 +95,7 @@ export const segmentize = ( const { byteStart, byteEnd } = facet.index; const features = facet.features; - if (byteStart > byteEnd || features.length === 0) { + if (byteStart > byteEnd || !features || features.length === 0) { continue; }