Merge branch 'bluesky-social:main' into main
This commit is contained in:
		
						commit
						38fd4282f8
					
				
					 38 changed files with 451 additions and 470 deletions
				
			
		|  | @ -211,7 +211,7 @@ | |||
|   <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="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="generator" content="bskyweb"> | ||||
|   <meta property="og:site_name" content="Bluesky Social" /> | ||||
|  |  | |||
|  | @ -176,43 +176,59 @@ export const atoms = { | |||
|   }, | ||||
|   text_2xs: { | ||||
|     fontSize: tokens.fontSize._2xs, | ||||
|     letterSpacing: 0.25, | ||||
|   }, | ||||
|   text_xs: { | ||||
|     fontSize: tokens.fontSize.xs, | ||||
|     letterSpacing: 0.25, | ||||
|   }, | ||||
|   text_sm: { | ||||
|     fontSize: tokens.fontSize.sm, | ||||
|     letterSpacing: 0.25, | ||||
|   }, | ||||
|   text_md: { | ||||
|     fontSize: tokens.fontSize.md, | ||||
|     letterSpacing: 0.25, | ||||
|   }, | ||||
|   text_lg: { | ||||
|     fontSize: tokens.fontSize.lg, | ||||
|     letterSpacing: 0.25, | ||||
|   }, | ||||
|   text_xl: { | ||||
|     fontSize: tokens.fontSize.xl, | ||||
|     letterSpacing: 0.25, | ||||
|   }, | ||||
|   text_2xl: { | ||||
|     fontSize: tokens.fontSize._2xl, | ||||
|     letterSpacing: 0.25, | ||||
|   }, | ||||
|   text_3xl: { | ||||
|     fontSize: tokens.fontSize._3xl, | ||||
|     letterSpacing: 0.25, | ||||
|   }, | ||||
|   text_4xl: { | ||||
|     fontSize: tokens.fontSize._4xl, | ||||
|     letterSpacing: 0.25, | ||||
|   }, | ||||
|   text_5xl: { | ||||
|     fontSize: tokens.fontSize._5xl, | ||||
|     letterSpacing: 0.25, | ||||
|   }, | ||||
|   leading_tight: { | ||||
|     lineHeight: 1.15, | ||||
|   }, | ||||
|   leading_snug: { | ||||
|     lineHeight: 1.25, | ||||
|     lineHeight: 1.3, | ||||
|   }, | ||||
|   leading_normal: { | ||||
|     lineHeight: 1.5, | ||||
|   }, | ||||
|   tracking_normal: { | ||||
|     letterSpacing: 0, | ||||
|   }, | ||||
|   tracking_wide: { | ||||
|     letterSpacing: 0.25, | ||||
|   }, | ||||
|   font_normal: { | ||||
|     fontWeight: tokens.fontWeight.normal, | ||||
|   }, | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ export function useColorModeTheme(): ThemeName { | |||
|   React.useLayoutEffect(() => { | ||||
|     const theme = getThemeName(colorScheme, colorMode, darkTheme) | ||||
|     updateDocument(theme) | ||||
|     updateSystemBackground(theme) | ||||
|     SystemUI.setBackgroundColorAsync(getBackgroundColor(theme)) | ||||
|   }, [colorMode, colorScheme, darkTheme]) | ||||
| 
 | ||||
|   return React.useMemo( | ||||
|  | @ -42,23 +42,24 @@ function updateDocument(theme: ThemeName) { | |||
|   if (isWeb && typeof window !== 'undefined') { | ||||
|     // @ts-ignore web only
 | ||||
|     const html = window.document.documentElement | ||||
|     // @ts-ignore web only
 | ||||
|     const meta = window.document.querySelector('meta[name="theme-color"]') | ||||
| 
 | ||||
|     // remove any other color mode classes
 | ||||
|     html.className = html.className.replace(/(theme)--\w+/g, '') | ||||
| 
 | ||||
|     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) { | ||||
|     case 'light': | ||||
|       SystemUI.setBackgroundColorAsync(light.atoms.bg.backgroundColor) | ||||
|       break | ||||
|       return light.atoms.bg.backgroundColor | ||||
|     case 'dark': | ||||
|       SystemUI.setBackgroundColorAsync(dark.atoms.bg.backgroundColor) | ||||
|       break | ||||
|       return dark.atoms.bg.backgroundColor | ||||
|     case 'dim': | ||||
|       SystemUI.setBackgroundColorAsync(dim.atoms.bg.backgroundColor) | ||||
|       break | ||||
|       return dim.atoms.bg.backgroundColor | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,7 +1,11 @@ | |||
| import React from 'react' | ||||
| 
 | ||||
| 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>({ | ||||
|   close: () => {}, | ||||
|  | @ -11,7 +15,7 @@ export function useDialogContext() { | |||
|   return React.useContext(Context) | ||||
| } | ||||
| 
 | ||||
| export function useDialogControl() { | ||||
| export function useDialogControl(): DialogOuterProps['control'] { | ||||
|   const id = React.useId() | ||||
|   const control = React.useRef<DialogControlProps>({ | ||||
|     open: () => {}, | ||||
|  | @ -30,6 +34,6 @@ export function useDialogControl() { | |||
|   return { | ||||
|     ref: control, | ||||
|     open: () => control.current.open(), | ||||
|     close: () => control.current.close(), | ||||
|     close: cb => control.current.close(cb), | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ import BottomSheet, { | |||
| } from '@gorhom/bottom-sheet' | ||||
| 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 {createInput} from '#/components/forms/TextField' | ||||
| 
 | ||||
|  | @ -35,12 +35,30 @@ export function Outer({ | |||
|   const sheetOptions = nativeOptions?.sheet || {} | ||||
|   const hasSnapPoints = !!sheetOptions.snapPoints | ||||
|   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() | ||||
|   }, []) | ||||
| 
 | ||||
|  | @ -56,15 +74,19 @@ export function Outer({ | |||
|   const onChange = React.useCallback( | ||||
|     (index: number) => { | ||||
|       if (index === -1) { | ||||
|         closeCallback.current?.() | ||||
|         closeCallback.current = undefined | ||||
|         onClose?.() | ||||
|         setOpenIndex(-1) | ||||
|       } | ||||
|     }, | ||||
|     [onClose], | ||||
|     [onClose, setOpenIndex], | ||||
|   ) | ||||
| 
 | ||||
|   const context = React.useMemo(() => ({close}), [close]) | ||||
| 
 | ||||
|   return ( | ||||
|     isOpen && ( | ||||
|       <Portal> | ||||
|         <BottomSheet | ||||
|           enableDynamicSizing={!hasSnapPoints} | ||||
|  | @ -74,8 +96,9 @@ export function Outer({ | |||
|           keyboardBlurBehavior="restore" | ||||
|           topInset={insets.top} | ||||
|           {...sheetOptions} | ||||
|           snapPoints={sheetOptions.snapPoints || ['100%']} | ||||
|           ref={sheet} | ||||
|         index={-1} | ||||
|           index={openIndex} | ||||
|           backgroundStyle={{backgroundColor: 'transparent'}} | ||||
|           backdropComponent={props => ( | ||||
|             <BottomSheetBackdrop | ||||
|  | @ -83,6 +106,7 @@ export function Outer({ | |||
|               appearsOnIndex={0} | ||||
|               disappearsOnIndex={-1} | ||||
|               {...props} | ||||
|               style={[flatten(props.style), t.atoms.bg_contrast_300]} | ||||
|             /> | ||||
|           )} | ||||
|           handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} | ||||
|  | @ -106,28 +130,29 @@ export function Outer({ | |||
|         </BottomSheet> | ||||
|       </Portal> | ||||
|     ) | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| // TODO a11y props here, or is that handled by the sheet?
 | ||||
| export function Inner(props: DialogInnerProps) { | ||||
| export function Inner({children, style}: DialogInnerProps) { | ||||
|   const insets = useSafeAreaInsets() | ||||
|   return ( | ||||
|     <BottomSheetView | ||||
|       style={[ | ||||
|         a.p_lg, | ||||
|         a.p_xl, | ||||
|         { | ||||
|           paddingTop: 40, | ||||
|           borderTopLeftRadius: 40, | ||||
|           borderTopRightRadius: 40, | ||||
|           paddingBottom: insets.bottom + a.pb_5xl.paddingBottom, | ||||
|         }, | ||||
|         flatten(style), | ||||
|       ]}> | ||||
|       {props.children} | ||||
|       {children} | ||||
|     </BottomSheetView> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| export function ScrollableInner(props: DialogInnerProps) { | ||||
| export function ScrollableInner({children, style}: DialogInnerProps) { | ||||
|   const insets = useSafeAreaInsets() | ||||
|   return ( | ||||
|     <BottomSheetScrollView | ||||
|  | @ -136,13 +161,15 @@ export function ScrollableInner(props: DialogInnerProps) { | |||
|       style={[ | ||||
|         a.flex_1, // main diff is this
 | ||||
|         a.p_xl, | ||||
|         a.h_full, | ||||
|         { | ||||
|           paddingTop: 40, | ||||
|           borderTopLeftRadius: 40, | ||||
|           borderTopRightRadius: 40, | ||||
|         }, | ||||
|         flatten(style), | ||||
|       ]}> | ||||
|       {props.children} | ||||
|       {children} | ||||
|       <View style={{height: insets.bottom + a.pt_5xl.paddingTop}} /> | ||||
|     </BottomSheetScrollView> | ||||
|   ) | ||||
|  |  | |||
|  | @ -5,11 +5,13 @@ import Animated, {FadeInDown, FadeIn} from 'react-native-reanimated' | |||
| import {msg} from '@lingui/macro' | ||||
| 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 {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types' | ||||
| 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 * from '#/components/Dialog/types' | ||||
|  | @ -18,9 +20,9 @@ export {Input} from '#/components/forms/TextField' | |||
| const stopPropagation = (e: any) => e.stopPropagation() | ||||
| 
 | ||||
| export function Outer({ | ||||
|   children, | ||||
|   control, | ||||
|   onClose, | ||||
|   children, | ||||
| }: React.PropsWithChildren<DialogOuterProps>) { | ||||
|   const {_} = useLingui() | ||||
|   const t = useTheme() | ||||
|  | @ -147,7 +149,7 @@ export function Inner({ | |||
|           a.rounded_md, | ||||
|           a.w_full, | ||||
|           a.border, | ||||
|           gtMobile ? a.p_xl : a.p_lg, | ||||
|           gtMobile ? a.p_2xl : a.p_xl, | ||||
|           t.atoms.bg, | ||||
|           { | ||||
|             maxWidth: 600, | ||||
|  | @ -156,7 +158,7 @@ export function Inner({ | |||
|             shadowOpacity: t.name === 'light' ? 0.1 : 0.4, | ||||
|             shadowRadius: 30, | ||||
|           }, | ||||
|           ...(Array.isArray(style) ? style : [style || {}]), | ||||
|           flatten(style), | ||||
|         ]}> | ||||
|         {children} | ||||
|       </Animated.View> | ||||
|  | @ -170,25 +172,28 @@ export function Handle() { | |||
|   return null | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * TODO(eric) unused rn | ||||
|  */ | ||||
| // export function Close() {
 | ||||
| //   const {_} = useLingui()
 | ||||
| //   const t = useTheme()
 | ||||
| //   const {close} = useDialogContext()
 | ||||
| //   return (
 | ||||
| //     <View
 | ||||
| //       style={[
 | ||||
| //         a.absolute,
 | ||||
| //         a.z_10,
 | ||||
| //         {
 | ||||
| //           top: a.pt_lg.paddingTop,
 | ||||
| //           right: a.pr_lg.paddingRight,
 | ||||
| //         },
 | ||||
| //       ]}>
 | ||||
| //       <Button onPress={close} label={_(msg`Close active dialog`)}>
 | ||||
| //       </Button>
 | ||||
| //     </View>
 | ||||
| //   )
 | ||||
| // }
 | ||||
| export function Close() { | ||||
|   const {_} = useLingui() | ||||
|   const {close} = React.useContext(Context) | ||||
|   return ( | ||||
|     <View | ||||
|       style={[ | ||||
|         a.absolute, | ||||
|         a.z_10, | ||||
|         { | ||||
|           top: a.pt_md.paddingTop, | ||||
|           right: a.pr_md.paddingRight, | ||||
|         }, | ||||
|       ]}> | ||||
|       <Button | ||||
|         size="small" | ||||
|         variant="ghost" | ||||
|         color="primary" | ||||
|         shape="round" | ||||
|         onPress={close} | ||||
|         label={_(msg`Close active dialog`)}> | ||||
|         <ButtonIcon icon={X} size="md" /> | ||||
|       </Button> | ||||
|     </View> | ||||
|   ) | ||||
| } | ||||
|  |  | |||
|  | @ -1,24 +1,34 @@ | |||
| import React from 'react' | ||||
| import type {ViewStyle, AccessibilityProps} from 'react-native' | ||||
| import type {AccessibilityProps} from 'react-native' | ||||
| import {BottomSheetProps} from '@gorhom/bottom-sheet' | ||||
| 
 | ||||
| import {ViewStyleProp} from '#/alf' | ||||
| 
 | ||||
| type A11yProps = Required<AccessibilityProps> | ||||
| 
 | ||||
| export type DialogContextProps = { | ||||
|   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 = { | ||||
|   open: (index?: number) => void | ||||
|   close: () => void | ||||
|   open: (options?: DialogControlOpenOptions) => void | ||||
|   close: (callback?: () => void) => void | ||||
| } | ||||
| 
 | ||||
| export type DialogOuterProps = { | ||||
|   control: { | ||||
|     ref: React.RefObject<DialogControlProps> | ||||
|     open: (index?: number) => void | ||||
|     close: () => void | ||||
|   } | ||||
|   } & DialogControlProps | ||||
|   onClose?: () => void | ||||
|   nativeOptions?: { | ||||
|     sheet?: Omit<BottomSheetProps, 'children'> | ||||
|  | @ -26,10 +36,7 @@ export type DialogOuterProps = { | |||
|   webOptions?: {} | ||||
| } | ||||
| 
 | ||||
| type DialogInnerPropsBase<T> = React.PropsWithChildren<{ | ||||
|   style?: ViewStyle | ||||
| }> & | ||||
|   T | ||||
| type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T | ||||
| export type DialogInnerProps = | ||||
|   | DialogInnerPropsBase<{ | ||||
|       label?: undefined | ||||
|  |  | |||
|  | @ -1,9 +1,5 @@ | |||
| import React from 'react' | ||||
| import { | ||||
|   GestureResponderEvent, | ||||
|   Linking, | ||||
|   TouchableWithoutFeedback, | ||||
| } from 'react-native' | ||||
| import {GestureResponderEvent, Linking} from 'react-native' | ||||
| import { | ||||
|   useLinkProps, | ||||
|   useNavigation, | ||||
|  | @ -23,7 +19,7 @@ import { | |||
| } from '#/lib/strings/url-helpers' | ||||
| import {useModalControls} from '#/state/modals' | ||||
| 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`. | ||||
|  | @ -55,11 +51,12 @@ type BaseLinkProps = Pick< | |||
|   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. | ||||
|    */ | ||||
|   onPress?: (e: GestureResponderEvent) => void | ||||
|   onPress?: (e: GestureResponderEvent) => void | false | ||||
| 
 | ||||
|   /** | ||||
|    * Web-only attribute. Sets `download` attr on web. | ||||
|  | @ -86,7 +83,9 @@ export function useLink({ | |||
| 
 | ||||
|   const onPress = React.useCallback( | ||||
|     (e: GestureResponderEvent) => { | ||||
|       outerOnPress?.(e) | ||||
|       const exitEarlyIfFalse = outerOnPress?.(e) | ||||
| 
 | ||||
|       if (exitEarlyIfFalse === false) return | ||||
| 
 | ||||
|       const requiresWarning = Boolean( | ||||
|         warnOnMismatchingTextChild && | ||||
|  | @ -217,7 +216,7 @@ export function Link({ | |||
| } | ||||
| 
 | ||||
| export type InlineLinkProps = React.PropsWithChildren< | ||||
|   BaseLinkProps & TextStyleProp | ||||
|   BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'> | ||||
| > | ||||
| 
 | ||||
| export function InlineLink({ | ||||
|  | @ -228,6 +227,7 @@ export function InlineLink({ | |||
|   style, | ||||
|   onPress: outerOnPress, | ||||
|   download, | ||||
|   selectable, | ||||
|   ...rest | ||||
| }: InlineLinkProps) { | ||||
|   const t = useTheme() | ||||
|  | @ -253,14 +253,8 @@ export function InlineLink({ | |||
|   const flattenedStyle = flatten(style) | ||||
| 
 | ||||
|   return ( | ||||
|     <TouchableWithoutFeedback | ||||
|       accessibilityRole="button" | ||||
|       onPress={download ? undefined : onPress} | ||||
|       onPressIn={onPressIn} | ||||
|       onPressOut={onPressOut} | ||||
|       onFocus={onFocus} | ||||
|       onBlur={onBlur}> | ||||
|     <Text | ||||
|       selectable={selectable} | ||||
|       label={href} | ||||
|       {...rest} | ||||
|       style={[ | ||||
|  | @ -273,6 +267,11 @@ export function InlineLink({ | |||
|         flattenedStyle, | ||||
|       ]} | ||||
|       role="link" | ||||
|       onPress={download ? undefined : onPress} | ||||
|       onPressIn={onPressIn} | ||||
|       onPressOut={onPressOut} | ||||
|       onFocus={onFocus} | ||||
|       onBlur={onBlur} | ||||
|       onMouseEnter={onHoverIn} | ||||
|       onMouseLeave={onHoverOut} | ||||
|       accessibilityRole="link" | ||||
|  | @ -290,6 +289,5 @@ export function InlineLink({ | |||
|       })}> | ||||
|       {children} | ||||
|     </Text> | ||||
|     </TouchableWithoutFeedback> | ||||
|   ) | ||||
| } | ||||
|  |  | |||
|  | @ -41,7 +41,7 @@ export function Outer({ | |||
|         <Dialog.Inner | ||||
|           accessibilityLabelledBy={titleId} | ||||
|           accessibilityDescribedBy={descriptionId} | ||||
|           style={{width: 'auto', maxWidth: 400}}> | ||||
|           style={[{width: 'auto', maxWidth: 400}]}> | ||||
|           {children} | ||||
|         </Dialog.Inner> | ||||
|       </Context.Provider> | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import React from 'react' | ||||
| 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 {Text} from '#/components/Typography' | ||||
| import {Text, TextProps} from '#/components/Typography' | ||||
| import {toShortUrl} from 'lib/strings/url-helpers' | ||||
| import {getAgent} from '#/state/session' | ||||
| 
 | ||||
|  | @ -16,18 +16,20 @@ export function RichText({ | |||
|   numberOfLines, | ||||
|   disableLinks, | ||||
|   resolveFacets = false, | ||||
| }: TextStyleProp & { | ||||
|   selectable, | ||||
| }: TextStyleProp & | ||||
|   Pick<TextProps, 'selectable'> & { | ||||
|     value: RichTextAPI | string | ||||
|     testID?: string | ||||
|     numberOfLines?: number | ||||
|     disableLinks?: boolean | ||||
|     resolveFacets?: boolean | ||||
| }) { | ||||
|   }) { | ||||
|   const detected = React.useRef(false) | ||||
|   const [richText, setRichText] = React.useState<RichTextAPI>(() => | ||||
|     value instanceof RichTextAPI ? value : new RichTextAPI({text: value}), | ||||
|   ) | ||||
|   const styles = [a.leading_normal, style] | ||||
|   const styles = [a.leading_snug, flatten(style)] | ||||
| 
 | ||||
|   React.useEffect(() => { | ||||
|     if (!resolveFacets) return | ||||
|  | @ -50,6 +52,7 @@ export function RichText({ | |||
|     if (text.length <= 5 && /^\p{Extended_Pictographic}+$/u.test(text)) { | ||||
|       return ( | ||||
|         <Text | ||||
|           selectable={selectable} | ||||
|           testID={testID} | ||||
|           style={[ | ||||
|             { | ||||
|  | @ -65,6 +68,7 @@ export function RichText({ | |||
|     } | ||||
|     return ( | ||||
|       <Text | ||||
|         selectable={selectable} | ||||
|         testID={testID} | ||||
|         style={styles} | ||||
|         numberOfLines={numberOfLines} | ||||
|  | @ -88,6 +92,7 @@ export function RichText({ | |||
|     ) { | ||||
|       els.push( | ||||
|         <InlineLink | ||||
|           selectable={selectable} | ||||
|           key={key} | ||||
|           to={`/profile/${mention.did}`} | ||||
|           style={[...styles, {pointerEvents: 'auto'}]} | ||||
|  | @ -102,6 +107,7 @@ export function RichText({ | |||
|       } else { | ||||
|         els.push( | ||||
|           <InlineLink | ||||
|             selectable={selectable} | ||||
|             key={key} | ||||
|             to={link.uri} | ||||
|             style={[...styles, {pointerEvents: 'auto'}]} | ||||
|  | @ -120,6 +126,7 @@ export function RichText({ | |||
| 
 | ||||
|   return ( | ||||
|     <Text | ||||
|       selectable={selectable} | ||||
|       testID={testID} | ||||
|       style={styles} | ||||
|       numberOfLines={numberOfLines} | ||||
|  |  | |||
|  | @ -1,7 +1,16 @@ | |||
| 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 {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 | ||||
|  | @ -44,27 +53,24 @@ function normalizeTextStyles(styles: TextStyle[]) { | |||
| /** | ||||
|  * 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 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}) { | ||||
|   return function HeadingElement({style, ...rest}: TextProps) { | ||||
|     const t = useTheme() | ||||
|     const attr = | ||||
|       web({ | ||||
|         role: 'heading', | ||||
|         'aria-level': level, | ||||
|       }) || {} | ||||
|     return ( | ||||
|       <RNText | ||||
|         {...attr} | ||||
|         {...rest} | ||||
|         style={normalizeTextStyles([t.atoms.text, flatten(style)])} | ||||
|       /> | ||||
|     ) | ||||
|     return <Text {...attr} {...rest} style={style} /> | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | @ -78,21 +84,15 @@ export const H4 = createHeadingElement({level: 4}) | |||
| export const H5 = createHeadingElement({level: 5}) | ||||
| export const H6 = createHeadingElement({level: 6}) | ||||
| export function P({style, ...rest}: TextProps) { | ||||
|   const t = useTheme() | ||||
|   const attr = | ||||
|     web({ | ||||
|       role: 'paragraph', | ||||
|     }) || {} | ||||
|   return ( | ||||
|     <RNText | ||||
|     <Text | ||||
|       {...attr} | ||||
|       {...rest} | ||||
|       style={normalizeTextStyles([ | ||||
|         atoms.text_md, | ||||
|         atoms.leading_normal, | ||||
|         t.atoms.text, | ||||
|         flatten(style), | ||||
|       ])} | ||||
|       style={[atoms.text_md, atoms.leading_normal, flatten(style)]} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
|  |  | |||
							
								
								
									
										5
									
								
								src/components/icons/Times.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/components/icons/Times.tsx
									
										
									
									
									
										Normal 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', | ||||
| }) | ||||
|  | @ -3,9 +3,8 @@ import {Insets, Platform} from 'react-native' | |||
| export const LOCAL_DEV_SERVICE = | ||||
|   Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583' | ||||
| export const STAGING_SERVICE = 'https://staging.bsky.dev' | ||||
| export const PROD_SERVICE = 'https://bsky.social' | ||||
| export const DEFAULT_SERVICE = PROD_SERVICE | ||||
| 
 | ||||
| export const BSKY_SERVICE = 'https://bsky.social' | ||||
| export const DEFAULT_SERVICE = BSKY_SERVICE | ||||
| const HELP_DESK_LANG = 'en-us' | ||||
| 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
 | ||||
| export const MAX_ALT_TEXT = 1000 | ||||
| 
 | ||||
| export function IS_LOCAL_DEV(url: string) { | ||||
|   return url.includes('localhost') | ||||
| export function IS_PROD_SERVICE(url?: string) { | ||||
|   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) => | ||||
|   `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 = { | ||||
|   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 function LINK_META_PROXY(serviceUrl: string) { | ||||
|   if (IS_LOCAL_DEV(serviceUrl)) { | ||||
|     return STAGING_LINK_META_PROXY | ||||
|   } else if (IS_STAGING(serviceUrl)) { | ||||
|     return STAGING_LINK_META_PROXY | ||||
|   } else { | ||||
|   if (IS_PROD_SERVICE(serviceUrl)) { | ||||
|     return PROD_LINK_META_PROXY | ||||
|   } | ||||
| 
 | ||||
|   return STAGING_LINK_META_PROXY | ||||
| } | ||||
| 
 | ||||
| export const STATUS_PAGE_URL = 'https://status.bsky.app/' | ||||
|  |  | |||
|  | @ -343,45 +343,45 @@ export function parseEmbedPlayerFromUrl( | |||
|   } | ||||
| } | ||||
| 
 | ||||
| export function getPlayerHeight({ | ||||
| export function getPlayerAspect({ | ||||
|   type, | ||||
|   width, | ||||
|   hasThumb, | ||||
|   width, | ||||
| }: { | ||||
|   type: EmbedPlayerParams['type'] | ||||
|   width: number | ||||
|   hasThumb: boolean | ||||
| }) { | ||||
|   if (!hasThumb) return (width / 16) * 9 | ||||
|   width: number | ||||
| }): {aspectRatio?: number; height?: number} { | ||||
|   if (!hasThumb) return {aspectRatio: 16 / 9} | ||||
| 
 | ||||
|   switch (type) { | ||||
|     case 'youtube_video': | ||||
|     case 'twitch_video': | ||||
|     case 'vimeo_video': | ||||
|       return (width / 16) * 9 | ||||
|       return {aspectRatio: 16 / 9} | ||||
|     case 'youtube_short': | ||||
|       if (SCREEN_HEIGHT < 600) { | ||||
|         return ((width / 9) * 16) / 1.75 | ||||
|         return {aspectRatio: (9 / 16) * 1.75} | ||||
|       } else { | ||||
|         return ((width / 9) * 16) / 1.5 | ||||
|         return {aspectRatio: (9 / 16) * 1.5} | ||||
|       } | ||||
|     case 'spotify_album': | ||||
|     case 'apple_music_album': | ||||
|     case 'apple_music_playlist': | ||||
|     case 'spotify_playlist': | ||||
|     case 'soundcloud_set': | ||||
|       return 380 | ||||
|       return {height: 380} | ||||
|     case 'spotify_song': | ||||
|       if (width <= 300) { | ||||
|         return 155 | ||||
|         return {height: 155} | ||||
|       } | ||||
|       return 232 | ||||
|       return {height: 232} | ||||
|     case 'soundcloud_track': | ||||
|       return 165 | ||||
|       return {height: 165} | ||||
|     case 'apple_music_song': | ||||
|       return 150 | ||||
|       return {height: 150} | ||||
|     default: | ||||
|       return width | ||||
|       return {aspectRatio: 16 / 9} | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import {AtUri} from '@atproto/api' | ||||
| import {PROD_SERVICE} from 'lib/constants' | ||||
| import {BSKY_SERVICE} from 'lib/constants' | ||||
| import TLDs from 'tlds' | ||||
| import psl from 'psl' | ||||
| 
 | ||||
|  | @ -28,7 +28,7 @@ export function makeRecordUri( | |||
| export function toNiceDomain(url: string): string { | ||||
|   try { | ||||
|     const urlp = new URL(url) | ||||
|     if (`https://${urlp.host}` === PROD_SERVICE) { | ||||
|     if (`https://${urlp.host}` === BSKY_SERVICE) { | ||||
|       return 'Bluesky Social' | ||||
|     } | ||||
|     return urlp.host ? urlp.host : url | ||||
|  |  | |||
|  | @ -3,7 +3,6 @@ import {View} from 'react-native' | |||
| import {useLingui} from '@lingui/react' | ||||
| import {msg, Trans} from '@lingui/macro' | ||||
| 
 | ||||
| import {IS_PROD} from '#/env' | ||||
| import {atoms as a} from '#/alf' | ||||
| import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' | ||||
| import {ListMagnifyingGlass_Stroke2_Corner0_Rounded as ListMagnifyingGlass} from '#/components/icons/ListMagnifyingGlass' | ||||
|  | @ -22,21 +21,28 @@ import { | |||
| import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard' | ||||
| import {aggregateInterestItems} from '#/screens/Onboarding/util' | ||||
| import {IconCircle} from '#/components/IconCircle' | ||||
| import {IS_PROD_SERVICE} from 'lib/constants' | ||||
| import {useSession} from 'state/session' | ||||
| 
 | ||||
| export function StepTopicalFeeds() { | ||||
|   const {_} = useLingui() | ||||
|   const {track} = useAnalytics() | ||||
|   const {currentAccount} = useSession() | ||||
|   const {state, dispatch, interestsDisplayNames} = React.useContext(Context) | ||||
|   const [selectedFeedUris, setSelectedFeedUris] = React.useState<string[]>([]) | ||||
|   const [saving, setSaving] = React.useState(false) | ||||
|   const suggestedFeedUris = React.useMemo(() => { | ||||
|     if (!IS_PROD) return [] | ||||
|     if (!IS_PROD_SERVICE(currentAccount?.service)) return [] | ||||
|     return aggregateInterestItems( | ||||
|       state.interestsStepResults.selectedInterests, | ||||
|       state.interestsStepResults.apiResponse.suggestedFeedUris, | ||||
|       state.interestsStepResults.apiResponse.suggestedFeedUris.default, | ||||
|     ).slice(0, 10) | ||||
|   }, [state.interestsStepResults]) | ||||
|   }, [ | ||||
|     currentAccount?.service, | ||||
|     state.interestsStepResults.apiResponse.suggestedFeedUris, | ||||
|     state.interestsStepResults.selectedInterests, | ||||
|   ]) | ||||
| 
 | ||||
|   const interestsText = React.useMemo(() => { | ||||
|     const i = state.interestsStepResults.selectedInterests.map( | ||||
|  |  | |||
|  | @ -133,23 +133,6 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) { | |||
|   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( | ||||
|   queryClient: QueryClient, | ||||
|   uri: string, | ||||
|  |  | |||
|  | @ -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( | ||||
|   queryClient: QueryClient, | ||||
|   uri: string, | ||||
|  |  | |||
|  | @ -8,8 +8,8 @@ import {useQuery, useQueryClient, QueryClient} from '@tanstack/react-query' | |||
| 
 | ||||
| import {getAgent} from '#/state/session' | ||||
| import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' | ||||
| import {findPostInQueryData as findPostInFeedQueryData} from './post-feed' | ||||
| import {findPostInQueryData as findPostInNotifsQueryData} from './notifications/feed' | ||||
| import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from './post-feed' | ||||
| import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from './notifications/feed' | ||||
| import {precacheThreadPostProfiles} from './profile' | ||||
| import {getEmbeddedPost} from './util' | ||||
| 
 | ||||
|  | @ -82,21 +82,9 @@ export function usePostThreadQuery(uri: string | undefined) { | |||
|         return undefined | ||||
|       } | ||||
|       { | ||||
|         const item = findPostInQueryData(queryClient, uri) | ||||
|         if (item) { | ||||
|           return threadNodeToPlaceholderThread(item) | ||||
|         } | ||||
|       } | ||||
|       { | ||||
|         const item = findPostInFeedQueryData(queryClient, uri) | ||||
|         if (item) { | ||||
|           return postViewToPlaceholderThread(item) | ||||
|         } | ||||
|       } | ||||
|       { | ||||
|         const item = findPostInNotifsQueryData(queryClient, uri) | ||||
|         if (item) { | ||||
|           return postViewToPlaceholderThread(item) | ||||
|         const post = findPostInQueryData(queryClient, uri) | ||||
|         if (post) { | ||||
|           return post | ||||
|         } | ||||
|       } | ||||
|       return undefined | ||||
|  | @ -171,11 +159,18 @@ function responseToThreadNodes( | |||
|     AppBskyFeedPost.isRecord(node.post.record) && | ||||
|     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 { | ||||
|       type: 'post', | ||||
|       _reactKey: node.post.uri, | ||||
|       uri: node.post.uri, | ||||
|       post: node.post, | ||||
|       post: post, | ||||
|       record: node.post.record, | ||||
|       parent: | ||||
|         node.parent && direction !== 'down' | ||||
|  | @ -213,14 +208,24 @@ function responseToThreadNodes( | |||
| function findPostInQueryData( | ||||
|   queryClient: QueryClient, | ||||
|   uri: string, | ||||
| ): ThreadNode | undefined { | ||||
|   const generator = findAllPostsInQueryData(queryClient, uri) | ||||
|   const result = generator.next() | ||||
|   if (result.done) { | ||||
|     return undefined | ||||
| ): ThreadNode | void { | ||||
|   let partial | ||||
|   for (let item of findAllPostsInQueryData(queryClient, uri)) { | ||||
|     if (item.type === 'post') { | ||||
|       // 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 { | ||||
|     return result.value | ||||
|         partial = item | ||||
|         // Keep searching, we might still find a full post in the cache.
 | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   return partial | ||||
| } | ||||
| 
 | ||||
| export function* findAllPostsInQueryData( | ||||
|  | @ -236,7 +241,10 @@ export function* findAllPostsInQueryData( | |||
|     } | ||||
|     for (const item of traverseThread(queryData)) { | ||||
|       if (item.uri === uri) { | ||||
|         yield item | ||||
|         const placeholder = threadNodeToPlaceholderThread(item) | ||||
|         if (placeholder) { | ||||
|           yield placeholder | ||||
|         } | ||||
|       } | ||||
|       const quotedPost = | ||||
|         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> { | ||||
|  |  | |||
|  | @ -53,5 +53,6 @@ export function embedViewRecordToPostView( | |||
|     record: v.value, | ||||
|     indexedAt: v.indexedAt, | ||||
|     labels: v.labels, | ||||
|     embed: v.embeds?.[0], | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ import {createFullHandle} from '#/lib/strings/handles' | |||
| import {cleanError} from '#/lib/strings/errors' | ||||
| import {useOnboardingDispatch} from '#/state/shell/onboarding' | ||||
| import {useSessionApi} from '#/state/session' | ||||
| import {DEFAULT_SERVICE, IS_PROD} from '#/lib/constants' | ||||
| import {DEFAULT_SERVICE, IS_PROD_SERVICE} from '#/lib/constants' | ||||
| import { | ||||
|   DEFAULT_PROD_FEEDS, | ||||
|   usePreferencesSetBirthDateMutation, | ||||
|  | @ -147,7 +147,7 @@ export function useSubmitCreateAccount( | |||
|             : undefined, | ||||
|         }) | ||||
|         setBirthDate({birthDate: uiState.birthDate}) | ||||
|         if (IS_PROD(uiState.serviceUrl)) { | ||||
|         if (IS_PROD_SERVICE(uiState.serviceUrl)) { | ||||
|           setSavedFeeds(DEFAULT_PROD_FEEDS) | ||||
|         } | ||||
|       } catch (e: any) { | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ import React from 'react' | |||
| import {View} from 'react-native' | ||||
| import {useLingui} from '@lingui/react' | ||||
| 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 {atoms as a, useBreakpoints, useTheme} from '#/alf' | ||||
|  | @ -26,7 +26,7 @@ export function ServerInputDialog({ | |||
|   const [pdsAddressHistory, setPdsAddressHistory] = React.useState<string[]>( | ||||
|     persisted.get('pdsAddressHistory') || [], | ||||
|   ) | ||||
|   const [fixedOption, setFixedOption] = React.useState([PROD_SERVICE]) | ||||
|   const [fixedOption, setFixedOption] = React.useState([BSKY_SERVICE]) | ||||
|   const [customAddress, setCustomAddress] = React.useState('') | ||||
| 
 | ||||
|   const onClose = React.useCallback(() => { | ||||
|  | @ -86,7 +86,7 @@ export function ServerInputDialog({ | |||
|             label="Preferences" | ||||
|             values={fixedOption} | ||||
|             onChange={setFixedOption}> | ||||
|             <ToggleButton.Button name={PROD_SERVICE} label={_(msg`Bluesky`)}> | ||||
|             <ToggleButton.Button name={BSKY_SERVICE} label={_(msg`Bluesky`)}> | ||||
|               {_(msg`Bluesky`)} | ||||
|             </ToggleButton.Button> | ||||
|             <ToggleButton.Button | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ import React from 'react' | |||
| import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native' | ||||
| import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' | ||||
| import {Text} from '../util/text/Text' | ||||
| import {RichText} from '../util/text/RichText' | ||||
| import {RichText} from '#/components/RichText' | ||||
| import {usePalette} from 'lib/hooks/usePalette' | ||||
| import {s} from 'lib/styles' | ||||
| import {UserAvatar} from '../util/UserAvatar' | ||||
|  | @ -25,6 +25,7 @@ import { | |||
| } from '#/state/queries/preferences' | ||||
| import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed' | ||||
| import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' | ||||
| import {useTheme} from '#/alf' | ||||
| 
 | ||||
| export function FeedSourceCard({ | ||||
|   feedUri, | ||||
|  | @ -82,6 +83,7 @@ export function FeedSourceCardLoaded({ | |||
|   pinOnSave?: boolean | ||||
|   showMinimalPlaceholder?: boolean | ||||
| }) { | ||||
|   const t = useTheme() | ||||
|   const pal = usePalette('default') | ||||
|   const {_} = useLingui() | ||||
|   const navigation = useNavigation<NavigationProp>() | ||||
|  | @ -266,8 +268,8 @@ export function FeedSourceCardLoaded({ | |||
| 
 | ||||
|       {showDescription && feed.description ? ( | ||||
|         <RichText | ||||
|           style={[pal.textLight, styles.description]} | ||||
|           richText={feed.description} | ||||
|           style={[t.atoms.text_contrast_high, styles.description]} | ||||
|           value={feed.description} | ||||
|           numberOfLines={3} | ||||
|         /> | ||||
|       ) : null} | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' | |||
| import {AtUri, AppBskyGraphDefs, RichText} from '@atproto/api' | ||||
| import {Link} from '../util/Link' | ||||
| 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 {s} from 'lib/styles' | ||||
| import {usePalette} from 'lib/hooks/usePalette' | ||||
|  | @ -12,6 +12,7 @@ import {sanitizeDisplayName} from 'lib/strings/display-names' | |||
| import {sanitizeHandle} from 'lib/strings/handles' | ||||
| import {makeProfileLink} from 'lib/routes/links' | ||||
| import {Trans} from '@lingui/macro' | ||||
| import {atoms as a} from '#/alf' | ||||
| 
 | ||||
| export const ListCard = ({ | ||||
|   testID, | ||||
|  | @ -119,9 +120,9 @@ export const ListCard = ({ | |||
|       {descriptionRichText ? ( | ||||
|         <View style={styles.details}> | ||||
|           <RichTextCom | ||||
|             style={[pal.text, s.flex1]} | ||||
|             style={[a.flex_1]} | ||||
|             numberOfLines={20} | ||||
|             richText={descriptionRichText} | ||||
|             value={descriptionRichText} | ||||
|           /> | ||||
|         </View> | ||||
|       ) : undefined} | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' | |||
| import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' | ||||
| import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn' | ||||
| import {Link, TextLink} from '../util/Link' | ||||
| import {RichText} from '../util/text/RichText' | ||||
| import {RichText} from '#/components/RichText' | ||||
| import {Text} from '../util/text/Text' | ||||
| import {PreviewableUserAvatar} from '../util/UserAvatar' | ||||
| import {s} from 'lib/styles' | ||||
|  | @ -44,6 +44,7 @@ import {ThreadPost} from '#/state/queries/post-thread' | |||
| import {useSession} from 'state/session' | ||||
| import {WhoCanReply} from '../threadgate/WhoCanReply' | ||||
| import {LoadingPlaceholder} from '../util/LoadingPlaceholder' | ||||
| import {atoms as a} from '#/alf' | ||||
| 
 | ||||
| export function PostThreadItem({ | ||||
|   post, | ||||
|  | @ -326,10 +327,8 @@ let PostThreadItemLoaded = ({ | |||
|                     styles.postTextLargeContainer, | ||||
|                   ]}> | ||||
|                   <RichText | ||||
|                     type="post-text-lg" | ||||
|                     richText={richText} | ||||
|                     lineHeight={1.3} | ||||
|                     style={s.flex1} | ||||
|                     value={richText} | ||||
|                     style={[a.flex_1, a.text_xl]} | ||||
|                     selectable | ||||
|                   /> | ||||
|                 </View> | ||||
|  | @ -522,10 +521,8 @@ let PostThreadItemLoaded = ({ | |||
|                 {richText?.text ? ( | ||||
|                   <View style={styles.postTextContainer}> | ||||
|                     <RichText | ||||
|                       type="post-text" | ||||
|                       richText={richText} | ||||
|                       style={[pal.text, s.flex1]} | ||||
|                       lineHeight={1.3} | ||||
|                       value={richText} | ||||
|                       style={[a.flex_1, a.text_md]} | ||||
|                       numberOfLines={limitLines ? MAX_POST_LINES : undefined} | ||||
|                     /> | ||||
|                   </View> | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ import {PostCtrls} from '../util/post-ctrls/PostCtrls' | |||
| import {ContentHider} from '../util/moderation/ContentHider' | ||||
| import {PostAlerts} from '../util/moderation/PostAlerts' | ||||
| import {Text} from '../util/text/Text' | ||||
| import {RichText} from '../util/text/RichText' | ||||
| import {RichText} from '#/components/RichText' | ||||
| import {PreviewableUserAvatar} from '../util/UserAvatar' | ||||
| import {s, colors} from 'lib/styles' | ||||
| 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 {Trans, msg} from '@lingui/macro' | ||||
| import {useLingui} from '@lingui/react' | ||||
| import {atoms as a} from '#/alf' | ||||
| 
 | ||||
| export function Post({ | ||||
|   post, | ||||
|  | @ -184,11 +185,9 @@ function PostInner({ | |||
|               <View style={styles.postTextContainer}> | ||||
|                 <RichText | ||||
|                   testID="postText" | ||||
|                   type="post-text" | ||||
|                   richText={richText} | ||||
|                   lineHeight={1.3} | ||||
|                   value={richText} | ||||
|                   numberOfLines={limitLines ? MAX_POST_LINES : undefined} | ||||
|                   style={s.flex1} | ||||
|                   style={[a.flex_1, a.text_md]} | ||||
|                 /> | ||||
|               </View> | ||||
|             ) : undefined} | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ import {PostCtrls} from '../util/post-ctrls/PostCtrls' | |||
| import {PostEmbeds} from '../util/post-embeds' | ||||
| import {ContentHider} from '../util/moderation/ContentHider' | ||||
| import {PostAlerts} from '../util/moderation/PostAlerts' | ||||
| import {RichText} from '../util/text/RichText' | ||||
| import {RichText} from '#/components/RichText' | ||||
| import {PreviewableUserAvatar} from '../util/UserAvatar' | ||||
| import {s} from 'lib/styles' | ||||
| import {usePalette} from 'lib/hooks/usePalette' | ||||
|  | @ -36,6 +36,7 @@ import {FeedNameText} from '../util/FeedInfoText' | |||
| import {useSession} from '#/state/session' | ||||
| import {Trans, msg} from '@lingui/macro' | ||||
| import {useLingui} from '@lingui/react' | ||||
| import {atoms as a} from '#/alf' | ||||
| 
 | ||||
| export function FeedItem({ | ||||
|   post, | ||||
|  | @ -347,11 +348,9 @@ let PostContent = ({ | |||
|         <View style={styles.postTextContainer}> | ||||
|           <RichText | ||||
|             testID="postText" | ||||
|             type="post-text" | ||||
|             richText={richText} | ||||
|             lineHeight={1.3} | ||||
|             value={richText} | ||||
|             numberOfLines={limitLines ? MAX_POST_LINES : undefined} | ||||
|             style={s.flex1} | ||||
|             style={[a.flex_1, a.text_md]} | ||||
|           /> | ||||
|         </View> | ||||
|       ) : undefined} | ||||
|  |  | |||
|  | @ -23,7 +23,7 @@ import * as Toast from '../util/Toast' | |||
| import {LoadingPlaceholder} from '../util/LoadingPlaceholder' | ||||
| import {Text} from '../util/text/Text' | ||||
| import {ThemedText} from '../util/text/ThemedText' | ||||
| import {RichText} from '../util/text/RichText' | ||||
| import {RichText} from '#/components/RichText' | ||||
| import {UserAvatar} from '../util/UserAvatar' | ||||
| import {UserBanner} from '../util/UserBanner' | ||||
| import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts' | ||||
|  | @ -56,6 +56,7 @@ import {Shadow} from '#/state/cache/types' | |||
| import {useRequireAuth} from '#/state/session' | ||||
| import {LabelInfo} from '../util/moderation/LabelInfo' | ||||
| import {useProfileShadow} from 'state/cache/profile-shadow' | ||||
| import {atoms as a} from '#/alf' | ||||
| 
 | ||||
| let ProfileHeaderLoading = (_props: {}): React.ReactNode => { | ||||
|   const pal = usePalette('default') | ||||
|  | @ -608,12 +609,12 @@ let ProfileHeader = ({ | |||
|               </Text> | ||||
|             </View> | ||||
|             {descriptionRT && !moderation.profile.blur ? ( | ||||
|               <View pointerEvents="auto"> | ||||
|               <View pointerEvents="auto" style={[styles.description]}> | ||||
|                 <RichText | ||||
|                   testID="profileHeaderDescription" | ||||
|                   style={[styles.description, pal.text]} | ||||
|                   style={[a.text_md]} | ||||
|                   numberOfLines={15} | ||||
|                   richText={descriptionRT} | ||||
|                   value={descriptionRT} | ||||
|                 /> | ||||
|               </View> | ||||
|             ) : undefined} | ||||
|  |  | |||
|  | @ -1,11 +1,14 @@ | |||
| import React from 'react' | ||||
| import {View} from 'react-native' | ||||
| import {View, ViewStyle} from 'react-native' | ||||
| 
 | ||||
| /** | ||||
|  * This utility function captures events and stops | ||||
|  * them from propagating upwards. | ||||
|  */ | ||||
| export function EventStopper({children}: React.PropsWithChildren<{}>) { | ||||
| export function EventStopper({ | ||||
|   children, | ||||
|   style, | ||||
| }: React.PropsWithChildren<{style?: ViewStyle | ViewStyle[]}>) { | ||||
|   const stop = (e: any) => { | ||||
|     e.stopPropagation() | ||||
|   } | ||||
|  | @ -15,7 +18,8 @@ export function EventStopper({children}: React.PropsWithChildren<{}>) { | |||
|       onTouchEnd={stop} | ||||
|       // @ts-ignore web only -prf
 | ||||
|       onClick={stop} | ||||
|       onKeyDown={stop}> | ||||
|       onKeyDown={stop} | ||||
|       style={style}> | ||||
|       {children} | ||||
|     </View> | ||||
|   ) | ||||
|  |  | |||
|  | @ -21,7 +21,7 @@ import {msg} from '@lingui/macro' | |||
| import {useLingui} from '@lingui/react' | ||||
| import {useNavigation} from '@react-navigation/native' | ||||
| 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 {isNative} from 'platform/detection' | ||||
| import {NavigationProp} from 'lib/routes/types' | ||||
|  | @ -67,14 +67,12 @@ function PlaceholderOverlay({ | |||
| 
 | ||||
| // This renders the webview/youtube player as a separate layer
 | ||||
| function Player({ | ||||
|   height, | ||||
|   params, | ||||
|   onLoad, | ||||
|   isPlayerActive, | ||||
| }: { | ||||
|   isPlayerActive: boolean | ||||
|   params: EmbedPlayerParams | ||||
|   height: number | ||||
|   onLoad: () => void | ||||
| }) { | ||||
|   // ensures we only load what's requested
 | ||||
|  | @ -91,9 +89,7 @@ function Player({ | |||
|   if (!isPlayerActive) return null | ||||
| 
 | ||||
|   return ( | ||||
|     <View style={[styles.layer, styles.playerLayer]}> | ||||
|       <EventStopper> | ||||
|         <View style={{height, width: '100%'}}> | ||||
|     <EventStopper style={[styles.layer, styles.playerLayer]}> | ||||
|       <WebView | ||||
|         javaScriptEnabled={true} | ||||
|         onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} | ||||
|  | @ -104,12 +100,10 @@ function Player({ | |||
|         nestedScrollEnabled | ||||
|         source={{uri: params.playerUri}} | ||||
|         onLoad={onLoad} | ||||
|         style={styles.webview} | ||||
|         setSupportMultipleWindows={false} // Prevent any redirects from opening a new window (ads)
 | ||||
|             style={[styles.webview, styles.topRadius]} | ||||
|       /> | ||||
|         </View> | ||||
|     </EventStopper> | ||||
|     </View> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
|  | @ -129,13 +123,16 @@ export function ExternalPlayer({ | |||
| 
 | ||||
|   const [isPlayerActive, setPlayerActive] = React.useState(false) | ||||
|   const [isLoading, setIsLoading] = React.useState(true) | ||||
|   const [dim, setDim] = React.useState({ | ||||
|     width: 0, | ||||
|     height: 0, | ||||
| 
 | ||||
|   const aspect = React.useMemo(() => { | ||||
|     return getPlayerAspect({ | ||||
|       type: params.type, | ||||
|       width: windowDims.width, | ||||
|       hasThumb: !!link.thumb, | ||||
|     }) | ||||
|   }, [params.type, windowDims.width, link.thumb]) | ||||
| 
 | ||||
|   const viewRef = useAnimatedRef() | ||||
| 
 | ||||
|   const frameCallback = useFrameCallback(() => { | ||||
|     const measurement = measure(viewRef) | ||||
|     if (!measurement) return | ||||
|  | @ -180,17 +177,6 @@ export function ExternalPlayer({ | |||
|     } | ||||
|   }, [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(() => { | ||||
|     setIsLoading(false) | ||||
|   }, []) | ||||
|  | @ -216,32 +202,11 @@ export function ExternalPlayer({ | |||
|     [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 ( | ||||
|     <Animated.View | ||||
|       ref={viewRef} | ||||
|       style={{height}} | ||||
|       collapsable={false} | ||||
|       onLayout={onLayout}> | ||||
|     <Animated.View ref={viewRef} collapsable={false} style={[aspect]}> | ||||
|       {link.thumb && (!isPlayerActive || isLoading) && ( | ||||
|         <Image | ||||
|           style={[ | ||||
|             { | ||||
|               width: dim.width, | ||||
|               height, | ||||
|             }, | ||||
|             styles.topRadius, | ||||
|           ]} | ||||
|           style={[{flex: 1}, styles.topRadius]} | ||||
|           source={{uri: link.thumb}} | ||||
|           accessibilityIgnoresInvertColors | ||||
|         /> | ||||
|  | @ -251,12 +216,7 @@ export function ExternalPlayer({ | |||
|         isPlayerActive={isPlayerActive} | ||||
|         onPress={onPlayPress} | ||||
|       /> | ||||
|       <Player | ||||
|         isPlayerActive={isPlayerActive} | ||||
|         params={params} | ||||
|         height={height} | ||||
|         onLoad={onLoad} | ||||
|       /> | ||||
|       <Player isPlayerActive={isPlayerActive} params={params} onLoad={onLoad} /> | ||||
|     </Animated.View> | ||||
|   ) | ||||
| } | ||||
|  |  | |||
|  | @ -20,7 +20,8 @@ import {PostAlerts} from '../moderation/PostAlerts' | |||
| import {makeProfileLink} from 'lib/routes/links' | ||||
| import {InfoCircleIcon} from 'lib/icons' | ||||
| 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({ | ||||
|   embed, | ||||
|  | @ -127,11 +128,10 @@ export function QuoteEmbed({ | |||
|       ) : null} | ||||
|       {richText ? ( | ||||
|         <RichText | ||||
|           richText={richText} | ||||
|           type="post-text" | ||||
|           style={pal.text} | ||||
|           value={richText} | ||||
|           style={[a.text_md]} | ||||
|           numberOfLines={20} | ||||
|           noLinks | ||||
|           disableLinks | ||||
|         /> | ||||
|       ) : null} | ||||
|       {embed && <PostEmbeds embed={embed} moderation={{}} />} | ||||
|  |  | |||
|  | @ -10,6 +10,9 @@ import {usePalette} from 'lib/hooks/usePalette' | |||
| 
 | ||||
| const WORD_WRAP = {wordWrap: 1} | ||||
| 
 | ||||
| /** | ||||
|  * @deprecated use `#/components/RichText` | ||||
|  */ | ||||
| export function RichText({ | ||||
|   testID, | ||||
|   type = 'md', | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ import {TextLink} from 'view/com/util/Link' | |||
| import {ListRef} from 'view/com/util/List' | ||||
| import {Button} from 'view/com/util/forms/Button' | ||||
| 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 {FAB} from 'view/com/util/fab/FAB' | ||||
| import {EmptyState} from 'view/com/util/EmptyState' | ||||
|  | @ -59,6 +59,7 @@ import {useComposerControls} from '#/state/shell/composer' | |||
| import {truncateAndInvalidate} from '#/state/queries/util' | ||||
| import {isNative} from '#/platform/detection' | ||||
| import {listenSoftReset} from '#/state/events' | ||||
| import {atoms as a} from '#/alf' | ||||
| 
 | ||||
| const SECTION_TITLES = ['Posts', 'About'] | ||||
| 
 | ||||
|  | @ -575,9 +576,8 @@ function AboutSection({ | |||
|         {feedInfo.description ? ( | ||||
|           <RichText | ||||
|             testID="listDescription" | ||||
|             type="lg" | ||||
|             style={pal.text} | ||||
|             richText={feedInfo.description} | ||||
|             style={[a.text_md]} | ||||
|             value={feedInfo.description} | ||||
|           /> | ||||
|         ) : ( | ||||
|           <Text type="lg" style={[{fontStyle: 'italic'}, pal.textLight]}> | ||||
|  |  | |||
|  | @ -14,7 +14,7 @@ import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown' | |||
| import {CenteredView} from 'view/com/util/Views' | ||||
| import {EmptyState} from 'view/com/util/EmptyState' | ||||
| 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 {TextLink} from 'view/com/util/Link' | ||||
| import {ListRef} from 'view/com/util/List' | ||||
|  | @ -60,6 +60,7 @@ import { | |||
| import {logger} from '#/logger' | ||||
| import {useAnalytics} from '#/lib/analytics/analytics' | ||||
| import {listenSoftReset} from '#/state/events' | ||||
| import {atoms as a} from '#/alf' | ||||
| 
 | ||||
| const SECTION_TITLES_CURATE = ['Posts', 'About'] | ||||
| const SECTION_TITLES_MOD = ['About'] | ||||
|  | @ -742,9 +743,8 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>( | |||
|             {descriptionRT ? ( | ||||
|               <RichText | ||||
|                 testID="listDescription" | ||||
|                 type="lg" | ||||
|                 style={pal.text} | ||||
|                 richText={descriptionRT} | ||||
|                 style={[a.text_md]} | ||||
|                 value={descriptionRT} | ||||
|               /> | ||||
|             ) : ( | ||||
|               <Text | ||||
|  |  | |||
|  | @ -76,7 +76,7 @@ export function ExportCarDialog({ | |||
|               This feature is in beta. You can read more about repository | ||||
|               exports in{' '} | ||||
|               <InlineLink | ||||
|                 to="https://atproto.com/blog/repo-export" | ||||
|                 to="https://docs.bsky.app/blog/repo-export" | ||||
|                 style={[a.text_sm]}> | ||||
|                 this blogpost. | ||||
|               </InlineLink> | ||||
|  |  | |||
|  | @ -9,7 +9,8 @@ import * as Prompt from '#/components/Prompt' | |||
| import {useDialogStateControlContext} from '#/state/dialogs' | ||||
| 
 | ||||
| export function Dialogs() { | ||||
|   const control = Dialog.useDialogControl() | ||||
|   const scrollable = Dialog.useDialogControl() | ||||
|   const basic = Dialog.useDialogControl() | ||||
|   const prompt = Prompt.usePromptControl() | ||||
|   const {closeAllDialogs} = useDialogStateControlContext() | ||||
| 
 | ||||
|  | @ -20,8 +21,31 @@ export function Dialogs() { | |||
|         color="secondary" | ||||
|         size="small" | ||||
|         onPress={() => { | ||||
|           control.open() | ||||
|           scrollable.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"> | ||||
|         Open basic dialog | ||||
|  | @ -48,9 +72,18 @@ export function Dialogs() { | |||
|         </Prompt.Actions> | ||||
|       </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 | ||||
|         control={control} | ||||
|         nativeOptions={{sheet: {snapPoints: ['90%']}}}> | ||||
|         control={scrollable} | ||||
|         nativeOptions={{sheet: {snapPoints: ['100%']}}}> | ||||
|         <Dialog.Handle /> | ||||
| 
 | ||||
|         <Dialog.ScrollableInner | ||||
|  | @ -77,9 +110,13 @@ export function Dialogs() { | |||
|                 variant="outline" | ||||
|                 color="primary" | ||||
|                 size="small" | ||||
|                 onPress={() => control.close()} | ||||
|                 onPress={() => | ||||
|                   scrollable.close(() => { | ||||
|                     console.log('CLOSED') | ||||
|                   }) | ||||
|                 } | ||||
|                 label="Open basic dialog"> | ||||
|                 Close basic dialog | ||||
|                 Close dialog | ||||
|               </Button> | ||||
|             </View> | ||||
|           </View> | ||||
|  |  | |||
|  | @ -8,7 +8,9 @@ import {RichText} from '#/components/RichText' | |||
| export function Typography() { | ||||
|   return ( | ||||
|     <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_3xl]}>atoms.text_3xl</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`} | ||||
|       /> | ||||
|       <RichText | ||||
|         selectable | ||||
|         resolveFacets | ||||
|         value={`This is rich text. It can have mentions like @bsky.app or links like https://bsky.social`} | ||||
|         style={[a.text_xl]} | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ | |||
| <html> | ||||
|   <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="theme-color"> | ||||
|     <!-- | ||||
|       This viewport works for phones with notches. | ||||
|       It's optimized for gestures by disabling global zoom. | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue