Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21123dab43 | |||
| e1d71bdbd6 | |||
| 5e14462a23 | |||
| 18b7c466cf |
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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} />
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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')
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -25,6 +25,7 @@ export type CommonNavigatorParams = {
|
||||
ProfileLabelerLikedBy: {name: string}
|
||||
Debug: undefined
|
||||
DebugMod: undefined
|
||||
DebugAppcom: undefined
|
||||
Log: undefined
|
||||
Support: undefined
|
||||
PrivacyPolicy: undefined
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user