Compare commits

...

4 Commits

Author SHA1 Message Date
Paul Frazee 21123dab43 Add ActorLabel 2024-07-06 03:07:04 +01:00
Paul Frazee e1d71bdbd6 Add ATProto Avatar 2024-07-06 03:07:04 +01:00
Paul Frazee 5e14462a23 Add error boundary and ATProto Embed 2024-07-06 03:07:04 +01:00
Paul Frazee 18b7c466cf Appcom: initial commit 2024-07-06 03:07:04 +01:00
19 changed files with 864 additions and 0 deletions
+1
View File
@@ -165,6 +165,7 @@
"react-avatar-editor": "^13.0.0",
"react-compiler-runtime": "file:./lib/react-compiler-runtime",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.13",
"react-keyed-flatten-children": "^3.0.0",
"react-native": "0.74.1",
"react-native-compressor": "^1.8.24",
+6
View File
@@ -65,6 +65,7 @@ import {
import {AccessibilitySettingsScreen} from './view/screens/AccessibilitySettings'
import {CommunityGuidelinesScreen} from './view/screens/CommunityGuidelines'
import {CopyrightPolicyScreen} from './view/screens/CopyrightPolicy'
import {DebugAppcomScreen} from './view/screens/DebugAppcom'
import {DebugModScreen} from './view/screens/DebugMod'
import {FeedsScreen} from './view/screens/Feeds'
import {HomeScreen} from './view/screens/Home'
@@ -233,6 +234,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => DebugModScreen}
options={{title: title(msg`Moderation states`), requireAuth: true}}
/>
<Stack.Screen
name="DebugAppcom"
getComponent={() => DebugAppcomScreen}
options={{title: title(msg`Application components`), requireAuth: true}}
/>
<Stack.Screen
name="Log"
getComponent={() => LogScreen}
+43
View File
@@ -0,0 +1,43 @@
import React from 'react'
import {View} from 'react-native'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '../Typography'
import type {AppComNode} from './types'
import {AppComponent} from './vocabulary'
export function AppComponentRegion({
tree,
origin,
}: {
tree: AppComNode
origin: string
}) {
const t = useTheme()
return (
<View style={[a.border, t.atoms.border_contrast_medium, a.rounded_sm]}>
<View
style={[
t.atoms.bg_contrast_25,
a.px_md,
a.py_sm,
{
borderTopLeftRadius: a.rounded_sm.borderRadius,
borderTopRightRadius: a.rounded_sm.borderRadius,
},
]}>
<Text
style={[
t.atoms.text_contrast_medium,
a.text_xs,
{fontFamily: 'monospace'},
]}>
{origin}
</Text>
</View>
<View style={[a.px_md, a.py_lg]}>
<AppComponent node={tree} />
</View>
</View>
)
}
+6
View File
@@ -0,0 +1,6 @@
export interface AppComNode {
type: string
key?: string
props?: Record<string, any>
children?: Array<AppComNode>
}
@@ -0,0 +1,54 @@
import React from 'react'
import {AtUri} from '@atproto/api'
import {z} from 'zod'
import {useProfileQuery} from '#/state/queries/profile'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {color} from './common'
import {Label} from './Label'
const actorLabelProps = z.object({
uri: z.string(),
field: z.enum(['handle', 'displayName', 'description']),
color,
size: z
.number()
.positive()
.default(16)
.transform(v => ({fontSize: v})),
lineHeight: z
.number()
.positive()
.default(1)
.transform(v => ({lineHeight: v})),
weight: z
.enum(['normal', 'semibold', 'bold'])
.default('normal')
.transform(v => ({fontWeight: v})),
})
export type ActorLabelProps = z.infer<typeof actorLabelProps>
export function ActorLabel(props: React.PropsWithChildren<ActorLabelProps>) {
const propsParsed = actorLabelProps.parse(props)
const urip = new AtUri(propsParsed.uri)
const {data: did} = useResolveDidQuery(urip.host)
const {data: profile} = useProfileQuery({did})
let text = ''
if (profile) {
if (props.field === 'handle') {
text = profile.handle
}
if (props.field === 'displayName') {
text = profile.displayName || profile.handle
}
if (props.field === 'description') {
text = profile.description || ''
}
}
// TODO loading, error
return <Label {...props} text={text} />
}
@@ -0,0 +1,24 @@
import React from 'react'
import {AtUri} from '@atproto/api'
import {z} from 'zod'
import {useProfileQuery} from '#/state/queries/profile'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {UserAvatar} from '#/view/com/util/UserAvatar'
const avatarProps = z.object({
uri: z.string(),
size: z.number().default(50),
})
export type AvatarProps = z.infer<typeof avatarProps>
export function Avatar(props: React.PropsWithChildren<AvatarProps>) {
const propsParsed = avatarProps.parse(props)
const urip = new AtUri(propsParsed.uri)
const {data: did} = useResolveDidQuery(urip.host)
const {data: profile} = useProfileQuery({did})
return <UserAvatar avatar={profile?.avatar} size={propsParsed.size} />
}
+71
View File
@@ -0,0 +1,71 @@
import React from 'react'
import {View} from 'react-native'
import {z} from 'zod'
import {pad} from './common'
import {useBackgroundColor, useBorderColor} from './hooks'
const boxProps = z.object({
pad,
corner: z
.union([
z
.number()
.positive()
.transform(v => ({borderRadius: v})),
z
.object({
tl: z.number().positive().optional(),
tr: z.number().positive().optional(),
bl: z.number().positive().optional(),
br: z.number().positive().optional(),
})
.transform(obj => ({
borderTopLeftRadius: obj.tl,
borderTopRightRadius: obj.tr,
borderBottomLeftRadius: obj.bl,
borderBottomRightRadius: obj.br,
})),
])
.optional(),
background: z
.enum([
'default',
'primary',
'secondary',
'positive',
'negative',
'inverted',
'none',
])
.optional(),
border: z
.enum([
'default',
'primary',
'secondary',
'positive',
'negative',
'inverted',
'none',
])
.optional(),
})
export type BoxProps = z.infer<typeof boxProps>
export function Box(props: React.PropsWithChildren<BoxProps>) {
const styles = boxProps.parse(props)
const backgroundColor = useBackgroundColor(styles.background)
const borderColor = useBorderColor(styles.border)
return (
<View style={[styles.pad, styles.corner, backgroundColor, borderColor]}>
{props.children}
</View>
)
}
@@ -0,0 +1,58 @@
import React from 'react'
import {View} from 'react-native'
import {AtUri} from '@atproto/api'
import {z} from 'zod'
import {usePostQuery} from '#/state/queries/post'
import {useProfileQuery} from '#/state/queries/profile'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {Post as PostInner} from '#/view/com/post/Post'
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {Text} from '#/components/Typography'
const embedProps = z.object({
uri: z.string(),
})
export type EmbedProps = z.infer<typeof embedProps>
export function Embed(props: React.PropsWithChildren<EmbedProps>) {
const propsParsed = embedProps.parse(props)
const urip = new AtUri(propsParsed.uri)
if (!urip.pathname || urip.pathname === '/') {
return <Actor actor={urip.host} />
}
if (urip.collection === 'app.bsky.feed.post') {
return <Post uri={urip.toString()} />
}
return <Unknown urip={urip} />
}
function Actor({actor}: {actor: string}) {
const {data: did} = useResolveDidQuery(actor)
const {data: profile} = useProfileQuery({did})
if (profile) {
return <ProfileCardWithFollowBtn noBg noBorder profile={profile} />
}
// TODO error, loading
return <View />
}
function Post({uri}: {uri: string}) {
const {data: post} = usePostQuery(uri)
if (post) {
return <PostInner post={post} hideTopBorder />
}
// TODO error, loading
return <View />
}
function Unknown({urip}: {urip: AtUri}) {
return (
<View style={{paddingVertical: 10, paddingHorizontal: 15}}>
<Text>Unsupported record type: {urip.collection}</Text>
</View>
)
}
@@ -0,0 +1,58 @@
import React from 'react'
import {View} from 'react-native'
import {z} from 'zod'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {
ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottom,
ChevronRight_Stroke2_Corner0_Rounded as ChevronRight,
} from '#/components/icons/Chevron'
import {Text} from '#/components/Typography'
const expandableProps = z.object({
label: z.string(),
defaultExpanded: z.boolean().default(false),
})
export type ExpandableProps = z.infer<typeof expandableProps>
export function Expandable(props: React.PropsWithChildren<ExpandableProps>) {
const t = useTheme()
const propsParsed = expandableProps.parse(props)
const [expanded, setExpanded] = React.useState(propsParsed.defaultExpanded)
return (
<View>
<Button onPress={() => setExpanded(v => !v)} label={propsParsed.label}>
{({hovered}) => (
<View
style={[
a.flex_1,
a.flex_row,
a.align_center,
a.gap_sm,
a.px_md,
a.py_md,
a.rounded_xs,
hovered && t.atoms.bg_contrast_25,
]}>
{expanded ? (
<ChevronBottom
width={14}
fill={t.atoms.text_contrast_low.color}
/>
) : (
<ChevronRight width={14} fill={t.atoms.text_contrast_low.color} />
)}
<Text style={[t.atoms.text, a.text_md]}>{props.label}</Text>
</View>
)}
</Button>
<View style={{display: expanded ? 'flex' : 'none'}}>
{props.children}
</View>
</View>
)
}
@@ -0,0 +1,37 @@
import React from 'react'
import {z} from 'zod'
import {Text} from '#/components/Typography'
import {color} from './common'
import {useFontColor} from './hooks'
export const labelProps = z.object({
text: z.string(),
color,
size: z
.number()
.positive()
.default(16)
.transform(v => ({fontSize: v})),
lineHeight: z
.number()
.positive()
.default(1)
.transform(v => ({lineHeight: v})),
weight: z
.enum(['normal', 'semibold', 'bold'])
.default('normal')
.transform(v => ({fontWeight: v})),
})
export type LabelProps = z.infer<typeof labelProps>
export function Label(props: React.PropsWithChildren<LabelProps>) {
const styles = labelProps.parse(props)
const fontColor = useFontColor(styles.color)
return (
<Text style={[fontColor, styles.size, styles.lineHeight, styles.weight]}>
{props.text}
</Text>
)
}
@@ -0,0 +1,67 @@
import React from 'react'
import {FlexAlignType, FlexStyle, View} from 'react-native'
import {z} from 'zod'
import {gap, pad} from './common'
const stackProps = z.object({
gap,
pad,
direction: z
.enum(['row', 'column'])
.default('column')
.transform(v => ({flexDirection: v})),
align: z
.enum(['start', 'center', 'end', 'stretch'])
.default('stretch')
.transform(v => {
if (v === 'start') {
return {alignItems: 'flex-start' as FlexAlignType}
}
if (v === 'end') {
return {alignItems: 'flex-end' as FlexAlignType}
}
return {alignItems: v as FlexAlignType}
}),
justify: z
.enum([
'start',
'center',
'end',
'space-between',
'space-around',
'space-evenly',
])
.default('start')
.transform(v => {
if (v === 'start') {
return {justifyContent: 'flex-start' as FlexStyle['justifyContent']}
}
if (v === 'end') {
return {justifyContent: 'flex-end' as FlexStyle['justifyContent']}
}
return {justifyContent: v as FlexStyle['justifyContent']}
}),
})
export type StackProps = z.infer<typeof stackProps>
export function Stack(props: React.PropsWithChildren<StackProps>) {
const styles = stackProps.parse(props)
return (
<View
style={[
{flexWrap: 'nowrap'},
styles.direction,
styles.align,
styles.justify,
styles.gap,
styles.pad,
]}>
{props.children}
</View>
)
}
+68
View File
@@ -0,0 +1,68 @@
import React from 'react'
import {View} from 'react-native'
import {z} from 'zod'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {Text} from '#/components/Typography'
const tabsProps = z.object({
labels: z.array(z.string()),
})
export type TabsProps = z.infer<typeof tabsProps>
export function Tabs(props: React.PropsWithChildren<TabsProps>) {
const t = useTheme()
const [selected, setSelected] = React.useState(0)
const propsParsed = tabsProps.parse(props)
return (
<View>
<View style={[a.flex_row]}>
{propsParsed.labels.map((label, i) => (
<Button
key={`${i}-${label}`}
onPress={() => setSelected(i)}
label={label}>
{({hovered}) => (
<View
style={[
a.px_lg,
a.py_md,
a.border_b,
hovered && t.atoms.bg_contrast_25,
{
borderColor:
hovered || selected === i
? t.palette.black
: 'transparent',
},
]}>
<Text style={[t.atoms.text, a.text_md]}>{label}</Text>
</View>
)}
</Button>
))}
</View>
{arr(props.children).map((child, i) => (
<View
key={`tab-content-${i}`}
style={{display: i === selected ? 'flex' : 'none'}}>
{child}
</View>
))}
</View>
)
}
function arr(v: any): Array<any> {
if (Array.isArray(v)) {
return v
}
if (!v) {
return []
}
return [v]
}
@@ -0,0 +1,36 @@
import {z} from 'zod'
export const gap = z
.number()
.optional()
.transform(v => ({gap: v}))
export const pad = z
.union([
z
.number()
.positive()
.transform(v => ({padding: v})),
z
.object({
x: z.number().positive().optional(),
y: z.number().positive().optional(),
t: z.number().positive().optional(),
b: z.number().positive().optional(),
l: z.number().positive().optional(),
r: z.number().positive().optional(),
})
.transform(obj => ({
paddingHorizontal: obj.x,
paddingVertical: obj.y,
paddingTop: obj.t,
paddingBottom: obj.b,
paddingLeft: obj.l,
paddingRight: obj.r,
})),
])
.optional()
export const color = z
.enum(['default', 'primary', 'secondary', 'positive', 'negative', 'inverted'])
.default('default')
+66
View File
@@ -0,0 +1,66 @@
import {useTheme} from '#/alf'
export function useBackgroundColor(value: string | undefined) {
const t = useTheme()
let background
if (value === 'default') {
background = t.atoms.bg
} else if (value === 'primary') {
background = {backgroundColor: t.palette.primary_500}
} else if (value === 'secondary') {
background = t.atoms.bg_contrast_100
} else if (value === 'positive') {
background = {backgroundColor: t.palette.positive_500}
} else if (value === 'negative') {
background = {backgroundColor: t.palette.negative_500}
} else if (value === 'inverted') {
background = t.atoms.bg_contrast_950
}
return background
}
export function useBorderColor(value: string | undefined) {
const t = useTheme()
let borderColor
if (value === 'default') {
borderColor = {
borderWidth: 1,
borderColor: t.atoms.border_contrast_medium.borderColor,
}
} else if (value === 'primary') {
borderColor = {borderWidth: 1, borderColor: t.palette.primary_500}
} else if (value === 'secondary') {
borderColor = {
borderWidth: 1,
borderColor: t.atoms.border_contrast_low.borderColor,
}
} else if (value === 'positive') {
borderColor = {borderWidth: 1, borderColor: t.palette.positive_500}
} else if (value === 'negative') {
borderColor = {borderWidth: 1, borderColor: t.palette.negative_500}
}
return borderColor
}
export function useFontColor(value: string | undefined) {
const t = useTheme()
let fontColor
if (value === 'default') {
fontColor = t.atoms.text
} else if (value === 'primary') {
fontColor = {color: t.palette.primary_500}
} else if (value === 'secondary') {
fontColor = t.atoms.text_contrast_medium
} else if (value === 'positive') {
fontColor = {color: t.palette.positive_500}
} else if (value === 'negative') {
fontColor = {color: t.palette.negative_500}
} else if (value === 'inverted') {
fontColor = {color: t.atoms.bg.backgroundColor}
}
return fontColor
}
@@ -0,0 +1,56 @@
import React from 'react'
import {View} from 'react-native'
import {ErrorBoundary} from 'react-error-boundary'
import {ZodError} from 'zod'
import {Text} from '#/components/Typography'
import type {AppComNode} from '../types'
import {ActorLabel} from './ActorLabel'
import {Avatar} from './Avatar'
import {Box} from './Box'
import {Embed} from './Embed'
import {Expandable} from './Expandable'
import {Label} from './Label'
import {Stack} from './Stack'
import {Tabs} from './Tabs'
export const VOCAB: Record<string, React.ComponentType<any>> = {
ActorLabel,
Avatar,
Box,
Embed,
Expandable,
Label,
Stack,
Tabs,
}
export function AppComponent({node}: {node: AppComNode}) {
const Com = VOCAB[node.type]
if (!Com) {
console.error('Unknown component:', node.type)
return <></>
}
return (
<ErrorBoundary fallbackRender={fallbackRender}>
<Com {...node.props}>
{node.children?.length
? node.children.map((child, i) => (
<AppComponent key={child.key || String(i)} node={child} />
))
: null}
</Com>
</ErrorBoundary>
)
}
function fallbackRender({error}: {error: Error}) {
const msg =
error instanceof ZodError
? `${error.issues[0].path.join('.')}: ${error.issues[0].message}`
: error.toString()
return (
<View>
<Text>{msg}</Text>
</View>
)
}
+1
View File
@@ -25,6 +25,7 @@ export type CommonNavigatorParams = {
ProfileLabelerLikedBy: {name: string}
Debug: undefined
DebugMod: undefined
DebugAppcom: undefined
Log: undefined
Support: undefined
PrivacyPolicy: undefined
+1
View File
@@ -25,6 +25,7 @@ export const router = new Router({
ProfileLabelerLikedBy: '/profile/:name/labeler/liked-by',
Debug: '/sys/debug',
DebugMod: '/sys/debug-mod',
DebugAppcom: '/sys/debug-appcom',
Log: '/sys/log',
AppPasswords: '/settings/app-passwords',
PreferencesFollowingFeed: '/settings/following-feed',
+204
View File
@@ -0,0 +1,204 @@
import React from 'react'
import {View} from 'react-native'
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {CenteredView, ScrollView} from '#/view/com/util/Views'
import {atoms as a, useTheme} from '#/alf'
import {AppComponentRegion} from '#/components/appcom/AppCom'
import * as TextField from '#/components/forms/TextField'
import {Text} from '#/components/Typography'
const DEFAULT_AC = h('Stack', {gap: 10}, [
h('Label', {
size: 36,
lineHeight: 1.2,
weight: 'bold',
text: 'Hello, world!',
}),
h('Label', {
lineHeight: 1.2,
text: 'This is the initial version of a fantastic new feature of the AT Protocol called "application components."',
}),
h('Tabs', {labels: ['Layout', 'Display', 'Inputs', 'Forms', 'ATProto']}, [
h('Stack', {gap: 10, pad: {t: 20}}, [
h('Box', {pad: {x: 10}}, [
h('Label', {text: 'Stack', size: 10, weight: 'bold'}),
]),
h('Box', {border: 'default', corner: 10, pad: {x: 2, y: 6}}, [
h(
'Stack',
{
direction: 'row',
align: 'center',
gap: 10,
pad: {x: 10},
},
[
h('Label', {text: 'One'}),
h('Label', {text: 'Two', color: 'positive'}),
h('Box', {border: 'default', corner: 4, pad: 10}, [
{type: 'Label', props: {text: 'Three'}},
]),
h('Box', {background: 'secondary', corner: 4, pad: 10}, [
{type: 'Label', props: {text: 'Four'}},
]),
h('Label', {text: 'Five', color: 'secondary'}),
],
),
]),
h('Box', {pad: {x: 10}}, [
h('Label', {text: 'Box', size: 10, weight: 'bold'}),
]),
h('Box', {border: 'default', corner: 10, pad: {x: 10, y: 14}}, [
h('Label', {
text: 'You can use the Box element as somewhat similar to a div.',
}),
h('Label', {text: 'Its children flow vertically with no gap.'}),
h('Label', {
text: 'What it offers is styling -- border, corner radius, background, and padding.',
}),
h('Label', {
text: 'If you want to control layout then you use a Stack, then if you want styling you use a Box.',
}),
h('Label', {
text: 'Boxes also make good neutral containers.',
}),
]),
h('Box', {pad: {x: 10}}, [
h('Label', {text: 'Expandable', size: 10, weight: 'bold'}),
]),
h('Box', {border: 'default', corner: 10, pad: 6}, [
h('Expandable', {label: 'Expandable'}, [h('Label', {text: 'Content'})]),
]),
]),
h('Label', {text: 'Display TODO'}),
h('Label', {text: 'Inputs TODO'}),
h('Label', {text: 'Forms TODO'}),
h('Stack', {gap: 10, pad: {t: 20}}, [
h('Box', {pad: {x: 10}}, [
h('Label', {text: 'Avatar', size: 10, weight: 'bold'}),
]),
h('Box', {border: 'secondary', corner: 8, pad: {x: 16, y: 12}}, [
h('Stack', {direction: 'row', gap: 10}, [
h('Avatar', {uri: 'bsky.app'}),
h('Avatar', {uri: 'at://atproto.com'}),
h('Avatar', {uri: 'at://pfrazee.com/'}),
]),
]),
h('Box', {pad: {x: 10}}, [
h('Label', {text: 'ActorLabel', size: 10, weight: 'bold'}),
]),
h('Box', {border: 'secondary', corner: 8, pad: {x: 16, y: 12}}, [
h('Stack', {gap: 10}, [
h('Stack', {direction: 'row', gap: 8}, [
h('Label', {weight: 'bold', text: 'Display Name:'}),
h('ActorLabel', {uri: 'bsky.app', field: 'displayName'}),
]),
h('Stack', {direction: 'row', gap: 8}, [
h('Label', {weight: 'bold', text: 'Handle:'}),
h('ActorLabel', {uri: 'bsky.app', field: 'handle'}),
]),
h('Stack', {direction: 'row', gap: 8}, [
h('Label', {weight: 'bold', text: 'Description:'}),
h('ActorLabel', {uri: 'bsky.app', field: 'description'}),
]),
]),
]),
h('Box', {pad: {x: 10}}, [
h('Label', {text: 'Embed (Actor)', size: 10, weight: 'bold'}),
]),
h('Box', {border: 'secondary', corner: 8}, [
h('Embed', {uri: 'bsky.app'}),
]),
h('Box', {border: 'secondary', corner: 8}, [
h('Embed', {uri: 'at://atproto.com/'}),
]),
h('Box', {border: 'secondary', corner: 8}, [
h('Embed', {uri: 'at://pfrazee.com'}),
]),
h('Box', {pad: {x: 10}}, [
h('Label', {text: 'Embed (Post)', size: 10, weight: 'bold'}),
]),
h('Box', {border: 'secondary', corner: 8}, [
h('Embed', {uri: 'at://pfrazee.com/app.bsky.feed.post/3ku7fbojcqs25'}),
]),
h('Box', {border: 'secondary', corner: 8}, [
h('Embed', {uri: 'at://bsky.app/app.bsky.feed.post/3ku73zs755e27'}),
]),
h('Box', {pad: {x: 10}}, [
h('Label', {
text: 'Embed (Unsupported record type)',
size: 10,
weight: 'bold',
}),
]),
h('Box', {border: 'secondary', corner: 8}, [
h('Embed', {uri: 'at://pfrazee.com/com.example.unknown/123'}),
]),
]),
]),
])
export const DebugAppcomScreen = ({}: NativeStackScreenProps<
CommonNavigatorParams,
'DebugAppcom'
>) => {
const t = useTheme()
const [acJson, setAcJson] = React.useState(
JSON.stringify(DEFAULT_AC, null, 2),
)
const [acObj, setAcObj] = React.useState(undefined)
const [error, setError] = React.useState('')
React.useEffect(() => {
try {
setAcObj(JSON.parse(acJson))
setError('')
} catch (e: any) {
setError(e.toString())
}
}, [acJson, setAcObj, setError])
return (
<ScrollView>
<CenteredView style={[t.atoms.bg, a.px_lg, a.py_lg]}>
<Text style={[a.text_5xl, a.font_bold, a.pb_lg]}>
Application components
</Text>
<TextField.LabelText>Application JSON component</TextField.LabelText>
<TextField.Input
multiline
numberOfLines={20}
value={acJson}
onChangeText={setAcJson}
label="Application component JSON"
/>
{error && (
<View
style={[
{backgroundColor: t.palette.negative_500},
a.px_lg,
a.py_md,
a.rounded_sm,
]}>
<Text style={{color: '#fff'}}>{error}</Text>
</View>
)}
<View style={{height: 40}} />
{acObj && <AppComponentRegion tree={acObj} origin="@bsky.app" />}
</CenteredView>
</ScrollView>
)
}
function h(type: string, props?: any | Array<any>, children?: Array<any>) {
return {
type,
props: Array.isArray(props) ? undefined : props,
children: Array.isArray(props) ? props : children,
}
}
+7
View File
@@ -18820,6 +18820,13 @@ react-dom@^18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.0"
react-error-boundary@^4.0.13:
version "4.0.13"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.13.tgz#80386b7b27b1131c5fbb7368b8c0d983354c7947"
integrity sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==
dependencies:
"@babel/runtime" "^7.12.5"
react-error-overlay@^6.0.11:
version "6.0.11"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"