Merge branch 'bluesky-social:main' into main

This commit is contained in:
Jan-Olof Eriksson 2024-02-21 13:22:13 +02:00 committed by GitHub
commit 38fd4282f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 451 additions and 470 deletions

View file

@ -211,7 +211,7 @@
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
<link rel="mask-icon" href="/static/safari-pinned-tab.svg" color="#1185fe"> <link rel="mask-icon" href="/static/safari-pinned-tab.svg" color="#1185fe">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color">
<meta name="application-name" content="Bluesky"> <meta name="application-name" content="Bluesky">
<meta name="generator" content="bskyweb"> <meta name="generator" content="bskyweb">
<meta property="og:site_name" content="Bluesky Social" /> <meta property="og:site_name" content="Bluesky Social" />

View file

@ -176,43 +176,59 @@ export const atoms = {
}, },
text_2xs: { text_2xs: {
fontSize: tokens.fontSize._2xs, fontSize: tokens.fontSize._2xs,
letterSpacing: 0.25,
}, },
text_xs: { text_xs: {
fontSize: tokens.fontSize.xs, fontSize: tokens.fontSize.xs,
letterSpacing: 0.25,
}, },
text_sm: { text_sm: {
fontSize: tokens.fontSize.sm, fontSize: tokens.fontSize.sm,
letterSpacing: 0.25,
}, },
text_md: { text_md: {
fontSize: tokens.fontSize.md, fontSize: tokens.fontSize.md,
letterSpacing: 0.25,
}, },
text_lg: { text_lg: {
fontSize: tokens.fontSize.lg, fontSize: tokens.fontSize.lg,
letterSpacing: 0.25,
}, },
text_xl: { text_xl: {
fontSize: tokens.fontSize.xl, fontSize: tokens.fontSize.xl,
letterSpacing: 0.25,
}, },
text_2xl: { text_2xl: {
fontSize: tokens.fontSize._2xl, fontSize: tokens.fontSize._2xl,
letterSpacing: 0.25,
}, },
text_3xl: { text_3xl: {
fontSize: tokens.fontSize._3xl, fontSize: tokens.fontSize._3xl,
letterSpacing: 0.25,
}, },
text_4xl: { text_4xl: {
fontSize: tokens.fontSize._4xl, fontSize: tokens.fontSize._4xl,
letterSpacing: 0.25,
}, },
text_5xl: { text_5xl: {
fontSize: tokens.fontSize._5xl, fontSize: tokens.fontSize._5xl,
letterSpacing: 0.25,
}, },
leading_tight: { leading_tight: {
lineHeight: 1.15, lineHeight: 1.15,
}, },
leading_snug: { leading_snug: {
lineHeight: 1.25, lineHeight: 1.3,
}, },
leading_normal: { leading_normal: {
lineHeight: 1.5, lineHeight: 1.5,
}, },
tracking_normal: {
letterSpacing: 0,
},
tracking_wide: {
letterSpacing: 0.25,
},
font_normal: { font_normal: {
fontWeight: tokens.fontWeight.normal, fontWeight: tokens.fontWeight.normal,
}, },

View file

@ -13,7 +13,7 @@ export function useColorModeTheme(): ThemeName {
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
const theme = getThemeName(colorScheme, colorMode, darkTheme) const theme = getThemeName(colorScheme, colorMode, darkTheme)
updateDocument(theme) updateDocument(theme)
updateSystemBackground(theme) SystemUI.setBackgroundColorAsync(getBackgroundColor(theme))
}, [colorMode, colorScheme, darkTheme]) }, [colorMode, colorScheme, darkTheme])
return React.useMemo( return React.useMemo(
@ -42,23 +42,24 @@ function updateDocument(theme: ThemeName) {
if (isWeb && typeof window !== 'undefined') { if (isWeb && typeof window !== 'undefined') {
// @ts-ignore web only // @ts-ignore web only
const html = window.document.documentElement const html = window.document.documentElement
// @ts-ignore web only
const meta = window.document.querySelector('meta[name="theme-color"]')
// remove any other color mode classes // remove any other color mode classes
html.className = html.className.replace(/(theme)--\w+/g, '') html.className = html.className.replace(/(theme)--\w+/g, '')
html.classList.add(`theme--${theme}`) html.classList.add(`theme--${theme}`)
// set color to 'theme-color' meta tag
meta?.setAttribute('content', getBackgroundColor(theme))
} }
} }
function updateSystemBackground(theme: ThemeName) { function getBackgroundColor(theme: ThemeName): string {
switch (theme) { switch (theme) {
case 'light': case 'light':
SystemUI.setBackgroundColorAsync(light.atoms.bg.backgroundColor) return light.atoms.bg.backgroundColor
break
case 'dark': case 'dark':
SystemUI.setBackgroundColorAsync(dark.atoms.bg.backgroundColor) return dark.atoms.bg.backgroundColor
break
case 'dim': case 'dim':
SystemUI.setBackgroundColorAsync(dim.atoms.bg.backgroundColor) return dim.atoms.bg.backgroundColor
break
} }
} }

View file

@ -1,7 +1,11 @@
import React from 'react' import React from 'react'
import {useDialogStateContext} from '#/state/dialogs' import {useDialogStateContext} from '#/state/dialogs'
import {DialogContextProps, DialogControlProps} from '#/components/Dialog/types' import {
DialogContextProps,
DialogControlProps,
DialogOuterProps,
} from '#/components/Dialog/types'
export const Context = React.createContext<DialogContextProps>({ export const Context = React.createContext<DialogContextProps>({
close: () => {}, close: () => {},
@ -11,7 +15,7 @@ export function useDialogContext() {
return React.useContext(Context) return React.useContext(Context)
} }
export function useDialogControl() { export function useDialogControl(): DialogOuterProps['control'] {
const id = React.useId() const id = React.useId()
const control = React.useRef<DialogControlProps>({ const control = React.useRef<DialogControlProps>({
open: () => {}, open: () => {},
@ -30,6 +34,6 @@ export function useDialogControl() {
return { return {
ref: control, ref: control,
open: () => control.current.open(), open: () => control.current.open(),
close: () => control.current.close(), close: cb => control.current.close(cb),
} }
} }

View file

@ -8,7 +8,7 @@ import BottomSheet, {
} from '@gorhom/bottom-sheet' } from '@gorhom/bottom-sheet'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useTheme, atoms as a} from '#/alf' import {useTheme, atoms as a, flatten} from '#/alf'
import {Portal} from '#/components/Portal' import {Portal} from '#/components/Portal'
import {createInput} from '#/components/forms/TextField' import {createInput} from '#/components/forms/TextField'
@ -35,12 +35,30 @@ export function Outer({
const sheetOptions = nativeOptions?.sheet || {} const sheetOptions = nativeOptions?.sheet || {}
const hasSnapPoints = !!sheetOptions.snapPoints const hasSnapPoints = !!sheetOptions.snapPoints
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const closeCallback = React.useRef<() => void>()
const open = React.useCallback<DialogControlProps['open']>((i = 0) => { /*
sheet.current?.snapToIndex(i) * Used to manage open/closed, but index is otherwise handled internally by `BottomSheet`
}, []) */
const [openIndex, setOpenIndex] = React.useState(-1)
const close = React.useCallback(() => { /*
* `openIndex` is the index of the snap point to open the bottom sheet to. If >0, the bottom sheet is open.
*/
const isOpen = openIndex > -1
const open = React.useCallback<DialogControlProps['open']>(
({index} = {}) => {
// can be set to any index of `snapPoints`, but `0` is the first i.e. "open"
setOpenIndex(index || 0)
},
[setOpenIndex],
)
const close = React.useCallback<DialogControlProps['close']>(cb => {
if (cb) {
closeCallback.current = cb
}
sheet.current?.close() sheet.current?.close()
}, []) }, [])
@ -56,15 +74,19 @@ export function Outer({
const onChange = React.useCallback( const onChange = React.useCallback(
(index: number) => { (index: number) => {
if (index === -1) { if (index === -1) {
closeCallback.current?.()
closeCallback.current = undefined
onClose?.() onClose?.()
setOpenIndex(-1)
} }
}, },
[onClose], [onClose, setOpenIndex],
) )
const context = React.useMemo(() => ({close}), [close]) const context = React.useMemo(() => ({close}), [close])
return ( return (
isOpen && (
<Portal> <Portal>
<BottomSheet <BottomSheet
enableDynamicSizing={!hasSnapPoints} enableDynamicSizing={!hasSnapPoints}
@ -74,8 +96,9 @@ export function Outer({
keyboardBlurBehavior="restore" keyboardBlurBehavior="restore"
topInset={insets.top} topInset={insets.top}
{...sheetOptions} {...sheetOptions}
snapPoints={sheetOptions.snapPoints || ['100%']}
ref={sheet} ref={sheet}
index={-1} index={openIndex}
backgroundStyle={{backgroundColor: 'transparent'}} backgroundStyle={{backgroundColor: 'transparent'}}
backdropComponent={props => ( backdropComponent={props => (
<BottomSheetBackdrop <BottomSheetBackdrop
@ -83,6 +106,7 @@ export function Outer({
appearsOnIndex={0} appearsOnIndex={0}
disappearsOnIndex={-1} disappearsOnIndex={-1}
{...props} {...props}
style={[flatten(props.style), t.atoms.bg_contrast_300]}
/> />
)} )}
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
@ -106,28 +130,29 @@ export function Outer({
</BottomSheet> </BottomSheet>
</Portal> </Portal>
) )
)
} }
// TODO a11y props here, or is that handled by the sheet? export function Inner({children, style}: DialogInnerProps) {
export function Inner(props: DialogInnerProps) {
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
return ( return (
<BottomSheetView <BottomSheetView
style={[ style={[
a.p_lg, a.p_xl,
{ {
paddingTop: 40, paddingTop: 40,
borderTopLeftRadius: 40, borderTopLeftRadius: 40,
borderTopRightRadius: 40, borderTopRightRadius: 40,
paddingBottom: insets.bottom + a.pb_5xl.paddingBottom, paddingBottom: insets.bottom + a.pb_5xl.paddingBottom,
}, },
flatten(style),
]}> ]}>
{props.children} {children}
</BottomSheetView> </BottomSheetView>
) )
} }
export function ScrollableInner(props: DialogInnerProps) { export function ScrollableInner({children, style}: DialogInnerProps) {
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
return ( return (
<BottomSheetScrollView <BottomSheetScrollView
@ -136,13 +161,15 @@ export function ScrollableInner(props: DialogInnerProps) {
style={[ style={[
a.flex_1, // main diff is this a.flex_1, // main diff is this
a.p_xl, a.p_xl,
a.h_full,
{ {
paddingTop: 40, paddingTop: 40,
borderTopLeftRadius: 40, borderTopLeftRadius: 40,
borderTopRightRadius: 40, borderTopRightRadius: 40,
}, },
flatten(style),
]}> ]}>
{props.children} {children}
<View style={{height: insets.bottom + a.pt_5xl.paddingTop}} /> <View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
</BottomSheetScrollView> </BottomSheetScrollView>
) )

View file

@ -5,11 +5,13 @@ import Animated, {FadeInDown, FadeIn} from 'react-native-reanimated'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useTheme, atoms as a, useBreakpoints, web} from '#/alf' import {useTheme, atoms as a, useBreakpoints, web, flatten} from '#/alf'
import {Portal} from '#/components/Portal' import {Portal} from '#/components/Portal'
import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types' import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types'
import {Context} from '#/components/Dialog/context' import {Context} from '#/components/Dialog/context'
import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
export {useDialogControl, useDialogContext} from '#/components/Dialog/context' export {useDialogControl, useDialogContext} from '#/components/Dialog/context'
export * from '#/components/Dialog/types' export * from '#/components/Dialog/types'
@ -18,9 +20,9 @@ export {Input} from '#/components/forms/TextField'
const stopPropagation = (e: any) => e.stopPropagation() const stopPropagation = (e: any) => e.stopPropagation()
export function Outer({ export function Outer({
children,
control, control,
onClose, onClose,
children,
}: React.PropsWithChildren<DialogOuterProps>) { }: React.PropsWithChildren<DialogOuterProps>) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
@ -147,7 +149,7 @@ export function Inner({
a.rounded_md, a.rounded_md,
a.w_full, a.w_full,
a.border, a.border,
gtMobile ? a.p_xl : a.p_lg, gtMobile ? a.p_2xl : a.p_xl,
t.atoms.bg, t.atoms.bg,
{ {
maxWidth: 600, maxWidth: 600,
@ -156,7 +158,7 @@ export function Inner({
shadowOpacity: t.name === 'light' ? 0.1 : 0.4, shadowOpacity: t.name === 'light' ? 0.1 : 0.4,
shadowRadius: 30, shadowRadius: 30,
}, },
...(Array.isArray(style) ? style : [style || {}]), flatten(style),
]}> ]}>
{children} {children}
</Animated.View> </Animated.View>
@ -170,25 +172,28 @@ export function Handle() {
return null return null
} }
/** export function Close() {
* TODO(eric) unused rn const {_} = useLingui()
*/ const {close} = React.useContext(Context)
// export function Close() { return (
// const {_} = useLingui() <View
// const t = useTheme() style={[
// const {close} = useDialogContext() a.absolute,
// return ( a.z_10,
// <View {
// style={[ top: a.pt_md.paddingTop,
// a.absolute, right: a.pr_md.paddingRight,
// a.z_10, },
// { ]}>
// top: a.pt_lg.paddingTop, <Button
// right: a.pr_lg.paddingRight, size="small"
// }, variant="ghost"
// ]}> color="primary"
// <Button onPress={close} label={_(msg`Close active dialog`)}> shape="round"
// </Button> onPress={close}
// </View> label={_(msg`Close active dialog`)}>
// ) <ButtonIcon icon={X} size="md" />
// } </Button>
</View>
)
}

View file

@ -1,24 +1,34 @@
import React from 'react' import React from 'react'
import type {ViewStyle, AccessibilityProps} from 'react-native' import type {AccessibilityProps} from 'react-native'
import {BottomSheetProps} from '@gorhom/bottom-sheet' import {BottomSheetProps} from '@gorhom/bottom-sheet'
import {ViewStyleProp} from '#/alf'
type A11yProps = Required<AccessibilityProps> type A11yProps = Required<AccessibilityProps>
export type DialogContextProps = { export type DialogContextProps = {
close: () => void close: () => void
} }
export type DialogControlOpenOptions = {
/**
* NATIVE ONLY
*
* Optional index of the snap point to open the bottom sheet to. Defaults to
* 0, which is the first snap point (i.e. "open").
*/
index?: number
}
export type DialogControlProps = { export type DialogControlProps = {
open: (index?: number) => void open: (options?: DialogControlOpenOptions) => void
close: () => void close: (callback?: () => void) => void
} }
export type DialogOuterProps = { export type DialogOuterProps = {
control: { control: {
ref: React.RefObject<DialogControlProps> ref: React.RefObject<DialogControlProps>
open: (index?: number) => void } & DialogControlProps
close: () => void
}
onClose?: () => void onClose?: () => void
nativeOptions?: { nativeOptions?: {
sheet?: Omit<BottomSheetProps, 'children'> sheet?: Omit<BottomSheetProps, 'children'>
@ -26,10 +36,7 @@ export type DialogOuterProps = {
webOptions?: {} webOptions?: {}
} }
type DialogInnerPropsBase<T> = React.PropsWithChildren<{ type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T
style?: ViewStyle
}> &
T
export type DialogInnerProps = export type DialogInnerProps =
| DialogInnerPropsBase<{ | DialogInnerPropsBase<{
label?: undefined label?: undefined

View file

@ -1,9 +1,5 @@
import React from 'react' import React from 'react'
import { import {GestureResponderEvent, Linking} from 'react-native'
GestureResponderEvent,
Linking,
TouchableWithoutFeedback,
} from 'react-native'
import { import {
useLinkProps, useLinkProps,
useNavigation, useNavigation,
@ -23,7 +19,7 @@ import {
} from '#/lib/strings/url-helpers' } from '#/lib/strings/url-helpers'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {router} from '#/routes' import {router} from '#/routes'
import {Text} from '#/components/Typography' import {Text, TextProps} from '#/components/Typography'
/** /**
* Only available within a `Link`, since that inherits from `Button`. * Only available within a `Link`, since that inherits from `Button`.
@ -55,11 +51,12 @@ type BaseLinkProps = Pick<
warnOnMismatchingTextChild?: boolean warnOnMismatchingTextChild?: boolean
/** /**
* Callback for when the link is pressed. * Callback for when the link is pressed. Prevent default and return `false`
* to exit early and prevent navigation.
* *
* DO NOT use this for navigation, that's what the `to` prop is for. * DO NOT use this for navigation, that's what the `to` prop is for.
*/ */
onPress?: (e: GestureResponderEvent) => void onPress?: (e: GestureResponderEvent) => void | false
/** /**
* Web-only attribute. Sets `download` attr on web. * Web-only attribute. Sets `download` attr on web.
@ -86,7 +83,9 @@ export function useLink({
const onPress = React.useCallback( const onPress = React.useCallback(
(e: GestureResponderEvent) => { (e: GestureResponderEvent) => {
outerOnPress?.(e) const exitEarlyIfFalse = outerOnPress?.(e)
if (exitEarlyIfFalse === false) return
const requiresWarning = Boolean( const requiresWarning = Boolean(
warnOnMismatchingTextChild && warnOnMismatchingTextChild &&
@ -217,7 +216,7 @@ export function Link({
} }
export type InlineLinkProps = React.PropsWithChildren< export type InlineLinkProps = React.PropsWithChildren<
BaseLinkProps & TextStyleProp BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
> >
export function InlineLink({ export function InlineLink({
@ -228,6 +227,7 @@ export function InlineLink({
style, style,
onPress: outerOnPress, onPress: outerOnPress,
download, download,
selectable,
...rest ...rest
}: InlineLinkProps) { }: InlineLinkProps) {
const t = useTheme() const t = useTheme()
@ -253,14 +253,8 @@ export function InlineLink({
const flattenedStyle = flatten(style) const flattenedStyle = flatten(style)
return ( return (
<TouchableWithoutFeedback
accessibilityRole="button"
onPress={download ? undefined : onPress}
onPressIn={onPressIn}
onPressOut={onPressOut}
onFocus={onFocus}
onBlur={onBlur}>
<Text <Text
selectable={selectable}
label={href} label={href}
{...rest} {...rest}
style={[ style={[
@ -273,6 +267,11 @@ export function InlineLink({
flattenedStyle, flattenedStyle,
]} ]}
role="link" role="link"
onPress={download ? undefined : onPress}
onPressIn={onPressIn}
onPressOut={onPressOut}
onFocus={onFocus}
onBlur={onBlur}
onMouseEnter={onHoverIn} onMouseEnter={onHoverIn}
onMouseLeave={onHoverOut} onMouseLeave={onHoverOut}
accessibilityRole="link" accessibilityRole="link"
@ -290,6 +289,5 @@ export function InlineLink({
})}> })}>
{children} {children}
</Text> </Text>
</TouchableWithoutFeedback>
) )
} }

View file

@ -41,7 +41,7 @@ export function Outer({
<Dialog.Inner <Dialog.Inner
accessibilityLabelledBy={titleId} accessibilityLabelledBy={titleId}
accessibilityDescribedBy={descriptionId} accessibilityDescribedBy={descriptionId}
style={{width: 'auto', maxWidth: 400}}> style={[{width: 'auto', maxWidth: 400}]}>
{children} {children}
</Dialog.Inner> </Dialog.Inner>
</Context.Provider> </Context.Provider>

View file

@ -1,9 +1,9 @@
import React from 'react' import React from 'react'
import {RichText as RichTextAPI, AppBskyRichtextFacet} from '@atproto/api' import {RichText as RichTextAPI, AppBskyRichtextFacet} from '@atproto/api'
import {atoms as a, TextStyleProp} from '#/alf' import {atoms as a, TextStyleProp, flatten} from '#/alf'
import {InlineLink} from '#/components/Link' import {InlineLink} from '#/components/Link'
import {Text} from '#/components/Typography' import {Text, TextProps} from '#/components/Typography'
import {toShortUrl} from 'lib/strings/url-helpers' import {toShortUrl} from 'lib/strings/url-helpers'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
@ -16,18 +16,20 @@ export function RichText({
numberOfLines, numberOfLines,
disableLinks, disableLinks,
resolveFacets = false, resolveFacets = false,
}: TextStyleProp & { selectable,
}: TextStyleProp &
Pick<TextProps, 'selectable'> & {
value: RichTextAPI | string value: RichTextAPI | string
testID?: string testID?: string
numberOfLines?: number numberOfLines?: number
disableLinks?: boolean disableLinks?: boolean
resolveFacets?: boolean resolveFacets?: boolean
}) { }) {
const detected = React.useRef(false) const detected = React.useRef(false)
const [richText, setRichText] = React.useState<RichTextAPI>(() => const [richText, setRichText] = React.useState<RichTextAPI>(() =>
value instanceof RichTextAPI ? value : new RichTextAPI({text: value}), value instanceof RichTextAPI ? value : new RichTextAPI({text: value}),
) )
const styles = [a.leading_normal, style] const styles = [a.leading_snug, flatten(style)]
React.useEffect(() => { React.useEffect(() => {
if (!resolveFacets) return if (!resolveFacets) return
@ -50,6 +52,7 @@ export function RichText({
if (text.length <= 5 && /^\p{Extended_Pictographic}+$/u.test(text)) { if (text.length <= 5 && /^\p{Extended_Pictographic}+$/u.test(text)) {
return ( return (
<Text <Text
selectable={selectable}
testID={testID} testID={testID}
style={[ style={[
{ {
@ -65,6 +68,7 @@ export function RichText({
} }
return ( return (
<Text <Text
selectable={selectable}
testID={testID} testID={testID}
style={styles} style={styles}
numberOfLines={numberOfLines} numberOfLines={numberOfLines}
@ -88,6 +92,7 @@ export function RichText({
) { ) {
els.push( els.push(
<InlineLink <InlineLink
selectable={selectable}
key={key} key={key}
to={`/profile/${mention.did}`} to={`/profile/${mention.did}`}
style={[...styles, {pointerEvents: 'auto'}]} style={[...styles, {pointerEvents: 'auto'}]}
@ -102,6 +107,7 @@ export function RichText({
} else { } else {
els.push( els.push(
<InlineLink <InlineLink
selectable={selectable}
key={key} key={key}
to={link.uri} to={link.uri}
style={[...styles, {pointerEvents: 'auto'}]} style={[...styles, {pointerEvents: 'auto'}]}
@ -120,6 +126,7 @@ export function RichText({
return ( return (
<Text <Text
selectable={selectable}
testID={testID} testID={testID}
style={styles} style={styles}
numberOfLines={numberOfLines} numberOfLines={numberOfLines}

View file

@ -1,7 +1,16 @@
import React from 'react' import React from 'react'
import {Text as RNText, TextStyle, TextProps} from 'react-native' import {Text as RNText, TextStyle, TextProps as RNTextProps} from 'react-native'
import {UITextView} from 'react-native-ui-text-view'
import {useTheme, atoms, web, flatten} from '#/alf' import {useTheme, atoms, web, flatten} from '#/alf'
import {isIOS} from '#/platform/detection'
export type TextProps = RNTextProps & {
/**
* Lets the user select text, to use the native copy and paste functionality.
*/
selectable?: boolean
}
/** /**
* Util to calculate lineHeight from a text size atom and a leading atom * Util to calculate lineHeight from a text size atom and a leading atom
@ -44,27 +53,24 @@ function normalizeTextStyles(styles: TextStyle[]) {
/** /**
* Our main text component. Use this most of the time. * Our main text component. Use this most of the time.
*/ */
export function Text({style, ...rest}: TextProps) { export function Text({style, selectable, ...rest}: TextProps) {
const t = useTheme() const t = useTheme()
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)]) const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)])
return <RNText style={s} {...rest} /> return selectable && isIOS ? (
<UITextView style={s} {...rest} />
) : (
<RNText selectable={selectable} style={s} {...rest} />
)
} }
export function createHeadingElement({level}: {level: number}) { export function createHeadingElement({level}: {level: number}) {
return function HeadingElement({style, ...rest}: TextProps) { return function HeadingElement({style, ...rest}: TextProps) {
const t = useTheme()
const attr = const attr =
web({ web({
role: 'heading', role: 'heading',
'aria-level': level, 'aria-level': level,
}) || {} }) || {}
return ( return <Text {...attr} {...rest} style={style} />
<RNText
{...attr}
{...rest}
style={normalizeTextStyles([t.atoms.text, flatten(style)])}
/>
)
} }
} }
@ -78,21 +84,15 @@ export const H4 = createHeadingElement({level: 4})
export const H5 = createHeadingElement({level: 5}) export const H5 = createHeadingElement({level: 5})
export const H6 = createHeadingElement({level: 6}) export const H6 = createHeadingElement({level: 6})
export function P({style, ...rest}: TextProps) { export function P({style, ...rest}: TextProps) {
const t = useTheme()
const attr = const attr =
web({ web({
role: 'paragraph', role: 'paragraph',
}) || {} }) || {}
return ( return (
<RNText <Text
{...attr} {...attr}
{...rest} {...rest}
style={normalizeTextStyles([ style={[atoms.text_md, atoms.leading_normal, flatten(style)]}
atoms.text_md,
atoms.leading_normal,
t.atoms.text,
flatten(style),
])}
/> />
) )
} }

View file

@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const TimesLarge_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M4.293 4.293a1 1 0 0 1 1.414 0L12 10.586l6.293-6.293a1 1 0 1 1 1.414 1.414L13.414 12l6.293 6.293a1 1 0 0 1-1.414 1.414L12 13.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L10.586 12 4.293 5.707a1 1 0 0 1 0-1.414Z',
})

View file

@ -3,9 +3,8 @@ import {Insets, Platform} from 'react-native'
export const LOCAL_DEV_SERVICE = export const LOCAL_DEV_SERVICE =
Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583' Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583'
export const STAGING_SERVICE = 'https://staging.bsky.dev' export const STAGING_SERVICE = 'https://staging.bsky.dev'
export const PROD_SERVICE = 'https://bsky.social' export const BSKY_SERVICE = 'https://bsky.social'
export const DEFAULT_SERVICE = PROD_SERVICE export const DEFAULT_SERVICE = BSKY_SERVICE
const HELP_DESK_LANG = 'en-us' const HELP_DESK_LANG = 'en-us'
export const HELP_DESK_URL = `https://blueskyweb.zendesk.com/hc/${HELP_DESK_LANG}` export const HELP_DESK_URL = `https://blueskyweb.zendesk.com/hc/${HELP_DESK_LANG}`
@ -36,92 +35,12 @@ export const MAX_GRAPHEME_LENGTH = 300
// but increasing limit per user feedback // but increasing limit per user feedback
export const MAX_ALT_TEXT = 1000 export const MAX_ALT_TEXT = 1000
export function IS_LOCAL_DEV(url: string) { export function IS_PROD_SERVICE(url?: string) {
return url.includes('localhost') return url && url !== STAGING_SERVICE && url !== LOCAL_DEV_SERVICE
} }
export function IS_STAGING(url: string) {
return url.startsWith('https://staging.bsky.dev')
}
export function IS_PROD(url: string) {
// NOTE
// until open federation, "production" is defined as the main server
// this definition will not work once federation is enabled!
// -prf
return (
url.startsWith('https://bsky.social') ||
url.startsWith('https://api.bsky.app') ||
/bsky\.network\/?$/.test(url)
)
}
export const PROD_TEAM_HANDLES = [
'jay.bsky.social',
'pfrazee.com',
'divy.zone',
'dholms.xyz',
'why.bsky.world',
'iamrosewang.bsky.social',
]
export const STAGING_TEAM_HANDLES = [
'arcalinea.staging.bsky.dev',
'paul.staging.bsky.dev',
'paul2.staging.bsky.dev',
]
export const DEV_TEAM_HANDLES = ['alice.test', 'bob.test', 'carla.test']
export function TEAM_HANDLES(serviceUrl: string) {
if (serviceUrl.includes('localhost')) {
return DEV_TEAM_HANDLES
} else if (serviceUrl.includes('staging')) {
return STAGING_TEAM_HANDLES
} else {
return PROD_TEAM_HANDLES
}
}
export const STAGING_DEFAULT_FEED = (rkey: string) =>
`at://did:plc:wqzurwm3kmaig6e6hnc2gqwo/app.bsky.feed.generator/${rkey}`
export const PROD_DEFAULT_FEED = (rkey: string) => export const PROD_DEFAULT_FEED = (rkey: string) =>
`at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/${rkey}` `at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/${rkey}`
export async function DEFAULT_FEEDS(
serviceUrl: string,
resolveHandle: (name: string) => Promise<string>,
) {
// TODO: remove this when the test suite no longer relies on it
if (IS_LOCAL_DEV(serviceUrl)) {
// local dev
const aliceDid = await resolveHandle('alice.test')
return {
pinned: [
`at://${aliceDid}/app.bsky.feed.generator/alice-favs`,
`at://${aliceDid}/app.bsky.feed.generator/alice-favs2`,
],
saved: [
`at://${aliceDid}/app.bsky.feed.generator/alice-favs`,
`at://${aliceDid}/app.bsky.feed.generator/alice-favs2`,
],
}
} else if (IS_STAGING(serviceUrl)) {
// staging
return {
pinned: [STAGING_DEFAULT_FEED('whats-hot')],
saved: [
STAGING_DEFAULT_FEED('bsky-team'),
STAGING_DEFAULT_FEED('with-friends'),
STAGING_DEFAULT_FEED('whats-hot'),
STAGING_DEFAULT_FEED('hot-classic'),
],
}
} else {
// production
return {
pinned: [PROD_DEFAULT_FEED('whats-hot')],
saved: [PROD_DEFAULT_FEED('whats-hot')],
}
}
}
export const POST_IMG_MAX = { export const POST_IMG_MAX = {
width: 2000, width: 2000,
@ -135,13 +54,11 @@ export const STAGING_LINK_META_PROXY =
export const PROD_LINK_META_PROXY = 'https://cardyb.bsky.app/v1/extract?url=' export const PROD_LINK_META_PROXY = 'https://cardyb.bsky.app/v1/extract?url='
export function LINK_META_PROXY(serviceUrl: string) { export function LINK_META_PROXY(serviceUrl: string) {
if (IS_LOCAL_DEV(serviceUrl)) { if (IS_PROD_SERVICE(serviceUrl)) {
return STAGING_LINK_META_PROXY
} else if (IS_STAGING(serviceUrl)) {
return STAGING_LINK_META_PROXY
} else {
return PROD_LINK_META_PROXY return PROD_LINK_META_PROXY
} }
return STAGING_LINK_META_PROXY
} }
export const STATUS_PAGE_URL = 'https://status.bsky.app/' export const STATUS_PAGE_URL = 'https://status.bsky.app/'

View file

@ -343,45 +343,45 @@ export function parseEmbedPlayerFromUrl(
} }
} }
export function getPlayerHeight({ export function getPlayerAspect({
type, type,
width,
hasThumb, hasThumb,
width,
}: { }: {
type: EmbedPlayerParams['type'] type: EmbedPlayerParams['type']
width: number
hasThumb: boolean hasThumb: boolean
}) { width: number
if (!hasThumb) return (width / 16) * 9 }): {aspectRatio?: number; height?: number} {
if (!hasThumb) return {aspectRatio: 16 / 9}
switch (type) { switch (type) {
case 'youtube_video': case 'youtube_video':
case 'twitch_video': case 'twitch_video':
case 'vimeo_video': case 'vimeo_video':
return (width / 16) * 9 return {aspectRatio: 16 / 9}
case 'youtube_short': case 'youtube_short':
if (SCREEN_HEIGHT < 600) { if (SCREEN_HEIGHT < 600) {
return ((width / 9) * 16) / 1.75 return {aspectRatio: (9 / 16) * 1.75}
} else { } else {
return ((width / 9) * 16) / 1.5 return {aspectRatio: (9 / 16) * 1.5}
} }
case 'spotify_album': case 'spotify_album':
case 'apple_music_album': case 'apple_music_album':
case 'apple_music_playlist': case 'apple_music_playlist':
case 'spotify_playlist': case 'spotify_playlist':
case 'soundcloud_set': case 'soundcloud_set':
return 380 return {height: 380}
case 'spotify_song': case 'spotify_song':
if (width <= 300) { if (width <= 300) {
return 155 return {height: 155}
} }
return 232 return {height: 232}
case 'soundcloud_track': case 'soundcloud_track':
return 165 return {height: 165}
case 'apple_music_song': case 'apple_music_song':
return 150 return {height: 150}
default: default:
return width return {aspectRatio: 16 / 9}
} }
} }

View file

@ -1,5 +1,5 @@
import {AtUri} from '@atproto/api' import {AtUri} from '@atproto/api'
import {PROD_SERVICE} from 'lib/constants' import {BSKY_SERVICE} from 'lib/constants'
import TLDs from 'tlds' import TLDs from 'tlds'
import psl from 'psl' import psl from 'psl'
@ -28,7 +28,7 @@ export function makeRecordUri(
export function toNiceDomain(url: string): string { export function toNiceDomain(url: string): string {
try { try {
const urlp = new URL(url) const urlp = new URL(url)
if (`https://${urlp.host}` === PROD_SERVICE) { if (`https://${urlp.host}` === BSKY_SERVICE) {
return 'Bluesky Social' return 'Bluesky Social'
} }
return urlp.host ? urlp.host : url return urlp.host ? urlp.host : url

View file

@ -3,7 +3,6 @@ import {View} from 'react-native'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {IS_PROD} from '#/env'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import {ListMagnifyingGlass_Stroke2_Corner0_Rounded as ListMagnifyingGlass} from '#/components/icons/ListMagnifyingGlass' import {ListMagnifyingGlass_Stroke2_Corner0_Rounded as ListMagnifyingGlass} from '#/components/icons/ListMagnifyingGlass'
@ -22,21 +21,28 @@ import {
import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard' import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard'
import {aggregateInterestItems} from '#/screens/Onboarding/util' import {aggregateInterestItems} from '#/screens/Onboarding/util'
import {IconCircle} from '#/components/IconCircle' import {IconCircle} from '#/components/IconCircle'
import {IS_PROD_SERVICE} from 'lib/constants'
import {useSession} from 'state/session'
export function StepTopicalFeeds() { export function StepTopicalFeeds() {
const {_} = useLingui() const {_} = useLingui()
const {track} = useAnalytics() const {track} = useAnalytics()
const {currentAccount} = useSession()
const {state, dispatch, interestsDisplayNames} = React.useContext(Context) const {state, dispatch, interestsDisplayNames} = React.useContext(Context)
const [selectedFeedUris, setSelectedFeedUris] = React.useState<string[]>([]) const [selectedFeedUris, setSelectedFeedUris] = React.useState<string[]>([])
const [saving, setSaving] = React.useState(false) const [saving, setSaving] = React.useState(false)
const suggestedFeedUris = React.useMemo(() => { const suggestedFeedUris = React.useMemo(() => {
if (!IS_PROD) return [] if (!IS_PROD_SERVICE(currentAccount?.service)) return []
return aggregateInterestItems( return aggregateInterestItems(
state.interestsStepResults.selectedInterests, state.interestsStepResults.selectedInterests,
state.interestsStepResults.apiResponse.suggestedFeedUris, state.interestsStepResults.apiResponse.suggestedFeedUris,
state.interestsStepResults.apiResponse.suggestedFeedUris.default, state.interestsStepResults.apiResponse.suggestedFeedUris.default,
).slice(0, 10) ).slice(0, 10)
}, [state.interestsStepResults]) }, [
currentAccount?.service,
state.interestsStepResults.apiResponse.suggestedFeedUris,
state.interestsStepResults.selectedInterests,
])
const interestsText = React.useMemo(() => { const interestsText = React.useMemo(() => {
const i = state.interestsStepResults.selectedInterests.map( const i = state.interestsStepResults.selectedInterests.map(

View file

@ -133,23 +133,6 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
return query return query
} }
/**
* This helper is used by the post-thread placeholder function to
* find a post in the query-data cache
*/
export function findPostInQueryData(
queryClient: QueryClient,
uri: string,
): AppBskyFeedDefs.PostView | undefined {
const generator = findAllPostsInQueryData(queryClient, uri)
const result = generator.next()
if (result.done) {
return undefined
} else {
return result.value
}
}
export function* findAllPostsInQueryData( export function* findAllPostsInQueryData(
queryClient: QueryClient, queryClient: QueryClient,
uri: string, uri: string,

View file

@ -365,23 +365,6 @@ function createApi(
} }
} }
/**
* This helper is used by the post-thread placeholder function to
* find a post in the query-data cache
*/
export function findPostInQueryData(
queryClient: QueryClient,
uri: string,
): AppBskyFeedDefs.PostView | undefined {
const generator = findAllPostsInQueryData(queryClient, uri)
const result = generator.next()
if (result.done) {
return undefined
} else {
return result.value
}
}
export function* findAllPostsInQueryData( export function* findAllPostsInQueryData(
queryClient: QueryClient, queryClient: QueryClient,
uri: string, uri: string,

View file

@ -8,8 +8,8 @@ import {useQuery, useQueryClient, QueryClient} from '@tanstack/react-query'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {findPostInQueryData as findPostInFeedQueryData} from './post-feed' import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from './post-feed'
import {findPostInQueryData as findPostInNotifsQueryData} from './notifications/feed' import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from './notifications/feed'
import {precacheThreadPostProfiles} from './profile' import {precacheThreadPostProfiles} from './profile'
import {getEmbeddedPost} from './util' import {getEmbeddedPost} from './util'
@ -82,21 +82,9 @@ export function usePostThreadQuery(uri: string | undefined) {
return undefined return undefined
} }
{ {
const item = findPostInQueryData(queryClient, uri) const post = findPostInQueryData(queryClient, uri)
if (item) { if (post) {
return threadNodeToPlaceholderThread(item) return post
}
}
{
const item = findPostInFeedQueryData(queryClient, uri)
if (item) {
return postViewToPlaceholderThread(item)
}
}
{
const item = findPostInNotifsQueryData(queryClient, uri)
if (item) {
return postViewToPlaceholderThread(item)
} }
} }
return undefined return undefined
@ -171,11 +159,18 @@ function responseToThreadNodes(
AppBskyFeedPost.isRecord(node.post.record) && AppBskyFeedPost.isRecord(node.post.record) &&
AppBskyFeedPost.validateRecord(node.post.record).success AppBskyFeedPost.validateRecord(node.post.record).success
) { ) {
const post = node.post
// These should normally be present. They're missing only for
// posts that were *just* created. Ideally, the backend would
// know to return zeros. Fill them in manually to compensate.
post.replyCount ??= 0
post.likeCount ??= 0
post.repostCount ??= 0
return { return {
type: 'post', type: 'post',
_reactKey: node.post.uri, _reactKey: node.post.uri,
uri: node.post.uri, uri: node.post.uri,
post: node.post, post: post,
record: node.post.record, record: node.post.record,
parent: parent:
node.parent && direction !== 'down' node.parent && direction !== 'down'
@ -213,14 +208,24 @@ function responseToThreadNodes(
function findPostInQueryData( function findPostInQueryData(
queryClient: QueryClient, queryClient: QueryClient,
uri: string, uri: string,
): ThreadNode | undefined { ): ThreadNode | void {
const generator = findAllPostsInQueryData(queryClient, uri) let partial
const result = generator.next() for (let item of findAllPostsInQueryData(queryClient, uri)) {
if (result.done) { if (item.type === 'post') {
return undefined // Currently, the backend doesn't send full post info in some cases
// (for example, for quoted posts). We use missing `likeCount`
// as a way to detect that. In the future, we should fix this on
// the backend, which will let us always stop on the first result.
const hasAllInfo = item.post.likeCount != null
if (hasAllInfo) {
return item
} else { } else {
return result.value partial = item
// Keep searching, we might still find a full post in the cache.
} }
}
}
return partial
} }
export function* findAllPostsInQueryData( export function* findAllPostsInQueryData(
@ -236,7 +241,10 @@ export function* findAllPostsInQueryData(
} }
for (const item of traverseThread(queryData)) { for (const item of traverseThread(queryData)) {
if (item.uri === uri) { if (item.uri === uri) {
yield item const placeholder = threadNodeToPlaceholderThread(item)
if (placeholder) {
yield placeholder
}
} }
const quotedPost = const quotedPost =
item.type === 'post' ? getEmbeddedPost(item.post.embed) : undefined item.type === 'post' ? getEmbeddedPost(item.post.embed) : undefined
@ -245,6 +253,12 @@ export function* findAllPostsInQueryData(
} }
} }
} }
for (let post of findAllPostsInFeedQueryData(queryClient, uri)) {
yield postViewToPlaceholderThread(post)
}
for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) {
yield postViewToPlaceholderThread(post)
}
} }
function* traverseThread(node: ThreadNode): Generator<ThreadNode, void> { function* traverseThread(node: ThreadNode): Generator<ThreadNode, void> {

View file

@ -53,5 +53,6 @@ export function embedViewRecordToPostView(
record: v.value, record: v.value,
indexedAt: v.indexedAt, indexedAt: v.indexedAt,
labels: v.labels, labels: v.labels,
embed: v.embeds?.[0],
} }
} }

View file

@ -12,7 +12,7 @@ import {createFullHandle} from '#/lib/strings/handles'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {useOnboardingDispatch} from '#/state/shell/onboarding' import {useOnboardingDispatch} from '#/state/shell/onboarding'
import {useSessionApi} from '#/state/session' import {useSessionApi} from '#/state/session'
import {DEFAULT_SERVICE, IS_PROD} from '#/lib/constants' import {DEFAULT_SERVICE, IS_PROD_SERVICE} from '#/lib/constants'
import { import {
DEFAULT_PROD_FEEDS, DEFAULT_PROD_FEEDS,
usePreferencesSetBirthDateMutation, usePreferencesSetBirthDateMutation,
@ -147,7 +147,7 @@ export function useSubmitCreateAccount(
: undefined, : undefined,
}) })
setBirthDate({birthDate: uiState.birthDate}) setBirthDate({birthDate: uiState.birthDate})
if (IS_PROD(uiState.serviceUrl)) { if (IS_PROD_SERVICE(uiState.serviceUrl)) {
setSavedFeeds(DEFAULT_PROD_FEEDS) setSavedFeeds(DEFAULT_PROD_FEEDS)
} }
} catch (e: any) { } catch (e: any) {

View file

@ -2,7 +2,7 @@ import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {PROD_SERVICE} from 'lib/constants' import {BSKY_SERVICE} from 'lib/constants'
import * as persisted from '#/state/persisted' import * as persisted from '#/state/persisted'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
@ -26,7 +26,7 @@ export function ServerInputDialog({
const [pdsAddressHistory, setPdsAddressHistory] = React.useState<string[]>( const [pdsAddressHistory, setPdsAddressHistory] = React.useState<string[]>(
persisted.get('pdsAddressHistory') || [], persisted.get('pdsAddressHistory') || [],
) )
const [fixedOption, setFixedOption] = React.useState([PROD_SERVICE]) const [fixedOption, setFixedOption] = React.useState([BSKY_SERVICE])
const [customAddress, setCustomAddress] = React.useState('') const [customAddress, setCustomAddress] = React.useState('')
const onClose = React.useCallback(() => { const onClose = React.useCallback(() => {
@ -86,7 +86,7 @@ export function ServerInputDialog({
label="Preferences" label="Preferences"
values={fixedOption} values={fixedOption}
onChange={setFixedOption}> onChange={setFixedOption}>
<ToggleButton.Button name={PROD_SERVICE} label={_(msg`Bluesky`)}> <ToggleButton.Button name={BSKY_SERVICE} label={_(msg`Bluesky`)}>
{_(msg`Bluesky`)} {_(msg`Bluesky`)}
</ToggleButton.Button> </ToggleButton.Button>
<ToggleButton.Button <ToggleButton.Button

View file

@ -2,7 +2,7 @@ import React from 'react'
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {RichText} from '../util/text/RichText' import {RichText} from '#/components/RichText'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
@ -25,6 +25,7 @@ import {
} from '#/state/queries/preferences' } from '#/state/queries/preferences'
import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed' import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {useTheme} from '#/alf'
export function FeedSourceCard({ export function FeedSourceCard({
feedUri, feedUri,
@ -82,6 +83,7 @@ export function FeedSourceCardLoaded({
pinOnSave?: boolean pinOnSave?: boolean
showMinimalPlaceholder?: boolean showMinimalPlaceholder?: boolean
}) { }) {
const t = useTheme()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
@ -266,8 +268,8 @@ export function FeedSourceCardLoaded({
{showDescription && feed.description ? ( {showDescription && feed.description ? (
<RichText <RichText
style={[pal.textLight, styles.description]} style={[t.atoms.text_contrast_high, styles.description]}
richText={feed.description} value={feed.description}
numberOfLines={3} numberOfLines={3}
/> />
) : null} ) : null}

View file

@ -3,7 +3,7 @@ import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {AtUri, AppBskyGraphDefs, RichText} from '@atproto/api' import {AtUri, AppBskyGraphDefs, RichText} from '@atproto/api'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {RichText as RichTextCom} from '../util/text/RichText' import {RichText as RichTextCom} from '#/components/RichText'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
@ -12,6 +12,7 @@ import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
import {atoms as a} from '#/alf'
export const ListCard = ({ export const ListCard = ({
testID, testID,
@ -119,9 +120,9 @@ export const ListCard = ({
{descriptionRichText ? ( {descriptionRichText ? (
<View style={styles.details}> <View style={styles.details}>
<RichTextCom <RichTextCom
style={[pal.text, s.flex1]} style={[a.flex_1]}
numberOfLines={20} numberOfLines={20}
richText={descriptionRichText} value={descriptionRichText}
/> />
</View> </View>
) : undefined} ) : undefined}

View file

@ -11,7 +11,7 @@ import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn' import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn'
import {Link, TextLink} from '../util/Link' import {Link, TextLink} from '../util/Link'
import {RichText} from '../util/text/RichText' import {RichText} from '#/components/RichText'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {PreviewableUserAvatar} from '../util/UserAvatar' import {PreviewableUserAvatar} from '../util/UserAvatar'
import {s} from 'lib/styles' import {s} from 'lib/styles'
@ -44,6 +44,7 @@ import {ThreadPost} from '#/state/queries/post-thread'
import {useSession} from 'state/session' import {useSession} from 'state/session'
import {WhoCanReply} from '../threadgate/WhoCanReply' import {WhoCanReply} from '../threadgate/WhoCanReply'
import {LoadingPlaceholder} from '../util/LoadingPlaceholder' import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
import {atoms as a} from '#/alf'
export function PostThreadItem({ export function PostThreadItem({
post, post,
@ -326,10 +327,8 @@ let PostThreadItemLoaded = ({
styles.postTextLargeContainer, styles.postTextLargeContainer,
]}> ]}>
<RichText <RichText
type="post-text-lg" value={richText}
richText={richText} style={[a.flex_1, a.text_xl]}
lineHeight={1.3}
style={s.flex1}
selectable selectable
/> />
</View> </View>
@ -522,10 +521,8 @@ let PostThreadItemLoaded = ({
{richText?.text ? ( {richText?.text ? (
<View style={styles.postTextContainer}> <View style={styles.postTextContainer}>
<RichText <RichText
type="post-text" value={richText}
richText={richText} style={[a.flex_1, a.text_md]}
style={[pal.text, s.flex1]}
lineHeight={1.3}
numberOfLines={limitLines ? MAX_POST_LINES : undefined} numberOfLines={limitLines ? MAX_POST_LINES : undefined}
/> />
</View> </View>

View file

@ -17,7 +17,7 @@ import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {ContentHider} from '../util/moderation/ContentHider' import {ContentHider} from '../util/moderation/ContentHider'
import {PostAlerts} from '../util/moderation/PostAlerts' import {PostAlerts} from '../util/moderation/PostAlerts'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {RichText} from '../util/text/RichText' import {RichText} from '#/components/RichText'
import {PreviewableUserAvatar} from '../util/UserAvatar' import {PreviewableUserAvatar} from '../util/UserAvatar'
import {s, colors} from 'lib/styles' import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
@ -29,6 +29,7 @@ import {useComposerControls} from '#/state/shell/composer'
import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {atoms as a} from '#/alf'
export function Post({ export function Post({
post, post,
@ -184,11 +185,9 @@ function PostInner({
<View style={styles.postTextContainer}> <View style={styles.postTextContainer}>
<RichText <RichText
testID="postText" testID="postText"
type="post-text" value={richText}
richText={richText}
lineHeight={1.3}
numberOfLines={limitLines ? MAX_POST_LINES : undefined} numberOfLines={limitLines ? MAX_POST_LINES : undefined}
style={s.flex1} style={[a.flex_1, a.text_md]}
/> />
</View> </View>
) : undefined} ) : undefined}

View file

@ -20,7 +20,7 @@ import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {PostEmbeds} from '../util/post-embeds' import {PostEmbeds} from '../util/post-embeds'
import {ContentHider} from '../util/moderation/ContentHider' import {ContentHider} from '../util/moderation/ContentHider'
import {PostAlerts} from '../util/moderation/PostAlerts' import {PostAlerts} from '../util/moderation/PostAlerts'
import {RichText} from '../util/text/RichText' import {RichText} from '#/components/RichText'
import {PreviewableUserAvatar} from '../util/UserAvatar' import {PreviewableUserAvatar} from '../util/UserAvatar'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
@ -36,6 +36,7 @@ import {FeedNameText} from '../util/FeedInfoText'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {atoms as a} from '#/alf'
export function FeedItem({ export function FeedItem({
post, post,
@ -347,11 +348,9 @@ let PostContent = ({
<View style={styles.postTextContainer}> <View style={styles.postTextContainer}>
<RichText <RichText
testID="postText" testID="postText"
type="post-text" value={richText}
richText={richText}
lineHeight={1.3}
numberOfLines={limitLines ? MAX_POST_LINES : undefined} numberOfLines={limitLines ? MAX_POST_LINES : undefined}
style={s.flex1} style={[a.flex_1, a.text_md]}
/> />
</View> </View>
) : undefined} ) : undefined}

View file

@ -23,7 +23,7 @@ import * as Toast from '../util/Toast'
import {LoadingPlaceholder} from '../util/LoadingPlaceholder' import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {ThemedText} from '../util/text/ThemedText' import {ThemedText} from '../util/text/ThemedText'
import {RichText} from '../util/text/RichText' import {RichText} from '#/components/RichText'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import {UserBanner} from '../util/UserBanner' import {UserBanner} from '../util/UserBanner'
import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts' import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts'
@ -56,6 +56,7 @@ import {Shadow} from '#/state/cache/types'
import {useRequireAuth} from '#/state/session' import {useRequireAuth} from '#/state/session'
import {LabelInfo} from '../util/moderation/LabelInfo' import {LabelInfo} from '../util/moderation/LabelInfo'
import {useProfileShadow} from 'state/cache/profile-shadow' import {useProfileShadow} from 'state/cache/profile-shadow'
import {atoms as a} from '#/alf'
let ProfileHeaderLoading = (_props: {}): React.ReactNode => { let ProfileHeaderLoading = (_props: {}): React.ReactNode => {
const pal = usePalette('default') const pal = usePalette('default')
@ -608,12 +609,12 @@ let ProfileHeader = ({
</Text> </Text>
</View> </View>
{descriptionRT && !moderation.profile.blur ? ( {descriptionRT && !moderation.profile.blur ? (
<View pointerEvents="auto"> <View pointerEvents="auto" style={[styles.description]}>
<RichText <RichText
testID="profileHeaderDescription" testID="profileHeaderDescription"
style={[styles.description, pal.text]} style={[a.text_md]}
numberOfLines={15} numberOfLines={15}
richText={descriptionRT} value={descriptionRT}
/> />
</View> </View>
) : undefined} ) : undefined}

View file

@ -1,11 +1,14 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View, ViewStyle} from 'react-native'
/** /**
* This utility function captures events and stops * This utility function captures events and stops
* them from propagating upwards. * them from propagating upwards.
*/ */
export function EventStopper({children}: React.PropsWithChildren<{}>) { export function EventStopper({
children,
style,
}: React.PropsWithChildren<{style?: ViewStyle | ViewStyle[]}>) {
const stop = (e: any) => { const stop = (e: any) => {
e.stopPropagation() e.stopPropagation()
} }
@ -15,7 +18,8 @@ export function EventStopper({children}: React.PropsWithChildren<{}>) {
onTouchEnd={stop} onTouchEnd={stop}
// @ts-ignore web only -prf // @ts-ignore web only -prf
onClick={stop} onClick={stop}
onKeyDown={stop}> onKeyDown={stop}
style={style}>
{children} {children}
</View> </View>
) )

View file

@ -21,7 +21,7 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {AppBskyEmbedExternal} from '@atproto/api' import {AppBskyEmbedExternal} from '@atproto/api'
import {EmbedPlayerParams, getPlayerHeight} from 'lib/strings/embed-player' import {EmbedPlayerParams, getPlayerAspect} from 'lib/strings/embed-player'
import {EventStopper} from '../EventStopper' import {EventStopper} from '../EventStopper'
import {isNative} from 'platform/detection' import {isNative} from 'platform/detection'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
@ -67,14 +67,12 @@ function PlaceholderOverlay({
// This renders the webview/youtube player as a separate layer // This renders the webview/youtube player as a separate layer
function Player({ function Player({
height,
params, params,
onLoad, onLoad,
isPlayerActive, isPlayerActive,
}: { }: {
isPlayerActive: boolean isPlayerActive: boolean
params: EmbedPlayerParams params: EmbedPlayerParams
height: number
onLoad: () => void onLoad: () => void
}) { }) {
// ensures we only load what's requested // ensures we only load what's requested
@ -91,9 +89,7 @@ function Player({
if (!isPlayerActive) return null if (!isPlayerActive) return null
return ( return (
<View style={[styles.layer, styles.playerLayer]}> <EventStopper style={[styles.layer, styles.playerLayer]}>
<EventStopper>
<View style={{height, width: '100%'}}>
<WebView <WebView
javaScriptEnabled={true} javaScriptEnabled={true}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
@ -104,12 +100,10 @@ function Player({
nestedScrollEnabled nestedScrollEnabled
source={{uri: params.playerUri}} source={{uri: params.playerUri}}
onLoad={onLoad} onLoad={onLoad}
style={styles.webview}
setSupportMultipleWindows={false} // Prevent any redirects from opening a new window (ads) setSupportMultipleWindows={false} // Prevent any redirects from opening a new window (ads)
style={[styles.webview, styles.topRadius]}
/> />
</View>
</EventStopper> </EventStopper>
</View>
) )
} }
@ -129,13 +123,16 @@ export function ExternalPlayer({
const [isPlayerActive, setPlayerActive] = React.useState(false) const [isPlayerActive, setPlayerActive] = React.useState(false)
const [isLoading, setIsLoading] = React.useState(true) const [isLoading, setIsLoading] = React.useState(true)
const [dim, setDim] = React.useState({
width: 0, const aspect = React.useMemo(() => {
height: 0, return getPlayerAspect({
type: params.type,
width: windowDims.width,
hasThumb: !!link.thumb,
}) })
}, [params.type, windowDims.width, link.thumb])
const viewRef = useAnimatedRef() const viewRef = useAnimatedRef()
const frameCallback = useFrameCallback(() => { const frameCallback = useFrameCallback(() => {
const measurement = measure(viewRef) const measurement = measure(viewRef)
if (!measurement) return if (!measurement) return
@ -180,17 +177,6 @@ export function ExternalPlayer({
} }
}, [navigation, isPlayerActive, frameCallback]) }, [navigation, isPlayerActive, frameCallback])
// calculate height for the player and the screen size
const height = React.useMemo(
() =>
getPlayerHeight({
type: params.type,
width: dim.width,
hasThumb: !!link.thumb,
}),
[params.type, dim.width, link.thumb],
)
const onLoad = React.useCallback(() => { const onLoad = React.useCallback(() => {
setIsLoading(false) setIsLoading(false)
}, []) }, [])
@ -216,32 +202,11 @@ export function ExternalPlayer({
[externalEmbedsPrefs, openModal, params.source], [externalEmbedsPrefs, openModal, params.source],
) )
// measure the layout to set sizing
const onLayout = React.useCallback(
(event: {nativeEvent: {layout: {width: any; height: any}}}) => {
setDim({
width: event.nativeEvent.layout.width,
height: event.nativeEvent.layout.height,
})
},
[],
)
return ( return (
<Animated.View <Animated.View ref={viewRef} collapsable={false} style={[aspect]}>
ref={viewRef}
style={{height}}
collapsable={false}
onLayout={onLayout}>
{link.thumb && (!isPlayerActive || isLoading) && ( {link.thumb && (!isPlayerActive || isLoading) && (
<Image <Image
style={[ style={[{flex: 1}, styles.topRadius]}
{
width: dim.width,
height,
},
styles.topRadius,
]}
source={{uri: link.thumb}} source={{uri: link.thumb}}
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
/> />
@ -251,12 +216,7 @@ export function ExternalPlayer({
isPlayerActive={isPlayerActive} isPlayerActive={isPlayerActive}
onPress={onPlayPress} onPress={onPlayPress}
/> />
<Player <Player isPlayerActive={isPlayerActive} params={params} onLoad={onLoad} />
isPlayerActive={isPlayerActive}
params={params}
height={height}
onLoad={onLoad}
/>
</Animated.View> </Animated.View>
) )
} }

View file

@ -20,7 +20,8 @@ import {PostAlerts} from '../moderation/PostAlerts'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {InfoCircleIcon} from 'lib/icons' import {InfoCircleIcon} from 'lib/icons'
import {Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
import {RichText} from 'view/com/util/text/RichText' import {RichText} from '#/components/RichText'
import {atoms as a} from '#/alf'
export function MaybeQuoteEmbed({ export function MaybeQuoteEmbed({
embed, embed,
@ -127,11 +128,10 @@ export function QuoteEmbed({
) : null} ) : null}
{richText ? ( {richText ? (
<RichText <RichText
richText={richText} value={richText}
type="post-text" style={[a.text_md]}
style={pal.text}
numberOfLines={20} numberOfLines={20}
noLinks disableLinks
/> />
) : null} ) : null}
{embed && <PostEmbeds embed={embed} moderation={{}} />} {embed && <PostEmbeds embed={embed} moderation={{}} />}

View file

@ -10,6 +10,9 @@ import {usePalette} from 'lib/hooks/usePalette'
const WORD_WRAP = {wordWrap: 1} const WORD_WRAP = {wordWrap: 1}
/**
* @deprecated use `#/components/RichText`
*/
export function RichText({ export function RichText({
testID, testID,
type = 'md', type = 'md',

View file

@ -17,7 +17,7 @@ import {TextLink} from 'view/com/util/Link'
import {ListRef} from 'view/com/util/List' import {ListRef} from 'view/com/util/List'
import {Button} from 'view/com/util/forms/Button' import {Button} from 'view/com/util/forms/Button'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {RichText} from 'view/com/util/text/RichText' import {RichText} from '#/components/RichText'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {FAB} from 'view/com/util/fab/FAB' import {FAB} from 'view/com/util/fab/FAB'
import {EmptyState} from 'view/com/util/EmptyState' import {EmptyState} from 'view/com/util/EmptyState'
@ -59,6 +59,7 @@ import {useComposerControls} from '#/state/shell/composer'
import {truncateAndInvalidate} from '#/state/queries/util' import {truncateAndInvalidate} from '#/state/queries/util'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {listenSoftReset} from '#/state/events' import {listenSoftReset} from '#/state/events'
import {atoms as a} from '#/alf'
const SECTION_TITLES = ['Posts', 'About'] const SECTION_TITLES = ['Posts', 'About']
@ -575,9 +576,8 @@ function AboutSection({
{feedInfo.description ? ( {feedInfo.description ? (
<RichText <RichText
testID="listDescription" testID="listDescription"
type="lg" style={[a.text_md]}
style={pal.text} value={feedInfo.description}
richText={feedInfo.description}
/> />
) : ( ) : (
<Text type="lg" style={[{fontStyle: 'italic'}, pal.textLight]}> <Text type="lg" style={[{fontStyle: 'italic'}, pal.textLight]}>

View file

@ -14,7 +14,7 @@ import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown'
import {CenteredView} from 'view/com/util/Views' import {CenteredView} from 'view/com/util/Views'
import {EmptyState} from 'view/com/util/EmptyState' import {EmptyState} from 'view/com/util/EmptyState'
import {LoadingScreen} from 'view/com/util/LoadingScreen' import {LoadingScreen} from 'view/com/util/LoadingScreen'
import {RichText} from 'view/com/util/text/RichText' import {RichText} from '#/components/RichText'
import {Button} from 'view/com/util/forms/Button' import {Button} from 'view/com/util/forms/Button'
import {TextLink} from 'view/com/util/Link' import {TextLink} from 'view/com/util/Link'
import {ListRef} from 'view/com/util/List' import {ListRef} from 'view/com/util/List'
@ -60,6 +60,7 @@ import {
import {logger} from '#/logger' import {logger} from '#/logger'
import {useAnalytics} from '#/lib/analytics/analytics' import {useAnalytics} from '#/lib/analytics/analytics'
import {listenSoftReset} from '#/state/events' import {listenSoftReset} from '#/state/events'
import {atoms as a} from '#/alf'
const SECTION_TITLES_CURATE = ['Posts', 'About'] const SECTION_TITLES_CURATE = ['Posts', 'About']
const SECTION_TITLES_MOD = ['About'] const SECTION_TITLES_MOD = ['About']
@ -742,9 +743,8 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
{descriptionRT ? ( {descriptionRT ? (
<RichText <RichText
testID="listDescription" testID="listDescription"
type="lg" style={[a.text_md]}
style={pal.text} value={descriptionRT}
richText={descriptionRT}
/> />
) : ( ) : (
<Text <Text

View file

@ -76,7 +76,7 @@ export function ExportCarDialog({
This feature is in beta. You can read more about repository This feature is in beta. You can read more about repository
exports in{' '} exports in{' '}
<InlineLink <InlineLink
to="https://atproto.com/blog/repo-export" to="https://docs.bsky.app/blog/repo-export"
style={[a.text_sm]}> style={[a.text_sm]}>
this blogpost. this blogpost.
</InlineLink> </InlineLink>

View file

@ -9,7 +9,8 @@ import * as Prompt from '#/components/Prompt'
import {useDialogStateControlContext} from '#/state/dialogs' import {useDialogStateControlContext} from '#/state/dialogs'
export function Dialogs() { export function Dialogs() {
const control = Dialog.useDialogControl() const scrollable = Dialog.useDialogControl()
const basic = Dialog.useDialogControl()
const prompt = Prompt.usePromptControl() const prompt = Prompt.usePromptControl()
const {closeAllDialogs} = useDialogStateControlContext() const {closeAllDialogs} = useDialogStateControlContext()
@ -20,8 +21,31 @@ export function Dialogs() {
color="secondary" color="secondary"
size="small" size="small"
onPress={() => { onPress={() => {
control.open() scrollable.open()
prompt.open() prompt.open()
basic.open()
}}
label="Open basic dialog">
Open all dialogs
</Button>
<Button
variant="outline"
color="secondary"
size="small"
onPress={() => {
scrollable.open()
}}
label="Open basic dialog">
Open scrollable dialog
</Button>
<Button
variant="outline"
color="secondary"
size="small"
onPress={() => {
basic.open()
}} }}
label="Open basic dialog"> label="Open basic dialog">
Open basic dialog Open basic dialog
@ -48,9 +72,18 @@ export function Dialogs() {
</Prompt.Actions> </Prompt.Actions>
</Prompt.Outer> </Prompt.Outer>
<Dialog.Outer control={basic}>
<Dialog.Handle />
<Dialog.Inner label="test">
<H3 nativeID="dialog-title">Dialog</H3>
<P nativeID="dialog-description">A basic dialog</P>
</Dialog.Inner>
</Dialog.Outer>
<Dialog.Outer <Dialog.Outer
control={control} control={scrollable}
nativeOptions={{sheet: {snapPoints: ['90%']}}}> nativeOptions={{sheet: {snapPoints: ['100%']}}}>
<Dialog.Handle /> <Dialog.Handle />
<Dialog.ScrollableInner <Dialog.ScrollableInner
@ -77,9 +110,13 @@ export function Dialogs() {
variant="outline" variant="outline"
color="primary" color="primary"
size="small" size="small"
onPress={() => control.close()} onPress={() =>
scrollable.close(() => {
console.log('CLOSED')
})
}
label="Open basic dialog"> label="Open basic dialog">
Close basic dialog Close dialog
</Button> </Button>
</View> </View>
</View> </View>

View file

@ -8,7 +8,9 @@ import {RichText} from '#/components/RichText'
export function Typography() { export function Typography() {
return ( return (
<View style={[a.gap_md]}> <View style={[a.gap_md]}>
<Text style={[a.text_5xl]}>atoms.text_5xl</Text> <Text selectable style={[a.text_5xl]}>
atoms.text_5xl
</Text>
<Text style={[a.text_4xl]}>atoms.text_4xl</Text> <Text style={[a.text_4xl]}>atoms.text_4xl</Text>
<Text style={[a.text_3xl]}>atoms.text_3xl</Text> <Text style={[a.text_3xl]}>atoms.text_3xl</Text>
<Text style={[a.text_2xl]}>atoms.text_2xl</Text> <Text style={[a.text_2xl]}>atoms.text_2xl</Text>
@ -24,6 +26,7 @@ export function Typography() {
value={`This is rich text. It can have mentions like @bsky.app or links like https://bsky.social`} value={`This is rich text. It can have mentions like @bsky.app or links like https://bsky.social`}
/> />
<RichText <RichText
selectable
resolveFacets resolveFacets
value={`This is rich text. It can have mentions like @bsky.app or links like https://bsky.social`} value={`This is rich text. It can have mentions like @bsky.app or links like https://bsky.social`}
style={[a.text_xl]} style={[a.text_xl]}

View file

@ -2,6 +2,7 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="theme-color">
<!-- <!--
This viewport works for phones with notches. This viewport works for phones with notches.
It's optimized for gestures by disabling global zoom. It's optimized for gestures by disabling global zoom.