diff options
author | Jan-Olof Eriksson <jan-olof.eriksson@iki.fi> | 2024-02-21 13:22:13 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-21 13:22:13 +0200 |
commit | 38fd4282f88ac020ee72d5a6324191ee80798450 (patch) | |
tree | 18f713fed78387f1cb83ac0818b4f1333fad6db8 /src | |
parent | 1269e76071ea7c79b93d1a58e80b74746c71ecd9 (diff) | |
parent | f88b16525498584f81ea7f594a63623fc5dc7ce9 (diff) | |
download | voidsky-38fd4282f88ac020ee72d5a6324191ee80798450.tar.zst |
Merge branch 'bluesky-social:main' into main
Diffstat (limited to 'src')
36 files changed, 449 insertions, 469 deletions
diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index f75e8ffe0..18f492d6e 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -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, }, diff --git a/src/alf/util/useColorModeTheme.ts b/src/alf/util/useColorModeTheme.ts index 48cf904fe..4f8921bf9 100644 --- a/src/alf/util/useColorModeTheme.ts +++ b/src/alf/util/useColorModeTheme.ts @@ -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 } } diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts index b28b9f5a2..f0c7c983a 100644 --- a/src/components/Dialog/context.ts +++ b/src/components/Dialog/context.ts @@ -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), } } diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index 9132e68de..27f43afd3 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -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) + + /* + * `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(() => { + const close = React.useCallback<DialogControlProps['close']>(cb => { + if (cb) { + closeCallback.current = cb + } sheet.current?.close() }, []) @@ -56,78 +74,85 @@ 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 ( - <Portal> - <BottomSheet - enableDynamicSizing={!hasSnapPoints} - enablePanDownToClose - keyboardBehavior="interactive" - android_keyboardInputMode="adjustResize" - keyboardBlurBehavior="restore" - topInset={insets.top} - {...sheetOptions} - ref={sheet} - index={-1} - backgroundStyle={{backgroundColor: 'transparent'}} - backdropComponent={props => ( - <BottomSheetBackdrop - opacity={0.4} - appearsOnIndex={0} - disappearsOnIndex={-1} - {...props} - /> - )} - handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} - handleStyle={{display: 'none'}} - onChange={onChange}> - <Context.Provider value={context}> - <View - style={[ - a.absolute, - a.inset_0, - t.atoms.bg, - { - borderTopLeftRadius: 40, - borderTopRightRadius: 40, - height: Dimensions.get('window').height * 2, - }, - ]} - /> - {children} - </Context.Provider> - </BottomSheet> - </Portal> + isOpen && ( + <Portal> + <BottomSheet + enableDynamicSizing={!hasSnapPoints} + enablePanDownToClose + keyboardBehavior="interactive" + android_keyboardInputMode="adjustResize" + keyboardBlurBehavior="restore" + topInset={insets.top} + {...sheetOptions} + snapPoints={sheetOptions.snapPoints || ['100%']} + ref={sheet} + index={openIndex} + backgroundStyle={{backgroundColor: 'transparent'}} + backdropComponent={props => ( + <BottomSheetBackdrop + opacity={0.4} + appearsOnIndex={0} + disappearsOnIndex={-1} + {...props} + style={[flatten(props.style), t.atoms.bg_contrast_300]} + /> + )} + handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} + handleStyle={{display: 'none'}} + onChange={onChange}> + <Context.Provider value={context}> + <View + style={[ + a.absolute, + a.inset_0, + t.atoms.bg, + { + borderTopLeftRadius: 40, + borderTopRightRadius: 40, + height: Dimensions.get('window').height * 2, + }, + ]} + /> + {children} + </Context.Provider> + </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> ) diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index 305c00e97..79441fb5e 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -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> + ) +} diff --git a/src/components/Dialog/types.ts b/src/components/Dialog/types.ts index d36784183..75ba825ac 100644 --- a/src/components/Dialog/types.ts +++ b/src/components/Dialog/types.ts @@ -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 diff --git a/src/components/Link.tsx b/src/components/Link.tsx index afd30b5ee..593b0863a 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -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,43 +253,41 @@ export function InlineLink({ const flattenedStyle = flatten(style) return ( - <TouchableWithoutFeedback - accessibilityRole="button" + <Text + selectable={selectable} + label={href} + {...rest} + style={[ + {color: t.palette.primary_500}, + (hovered || focused || pressed) && { + outline: 0, + textDecorationLine: 'underline', + textDecorationColor: flattenedStyle.color ?? t.palette.primary_500, + }, + flattenedStyle, + ]} + role="link" onPress={download ? undefined : onPress} onPressIn={onPressIn} onPressOut={onPressOut} onFocus={onFocus} - onBlur={onBlur}> - <Text - label={href} - {...rest} - style={[ - {color: t.palette.primary_500}, - (hovered || focused || pressed) && { - outline: 0, - textDecorationLine: 'underline', - textDecorationColor: flattenedStyle.color ?? t.palette.primary_500, - }, - flattenedStyle, - ]} - role="link" - onMouseEnter={onHoverIn} - onMouseLeave={onHoverOut} - accessibilityRole="link" - href={href} - {...web({ - hrefAttrs: { - target: download ? undefined : isExternal ? 'blank' : undefined, - rel: isExternal ? 'noopener noreferrer' : undefined, - download, - }, - dataSet: { - // default to no underline, apply this ourselves - noUnderline: '1', - }, - })}> - {children} - </Text> - </TouchableWithoutFeedback> + onBlur={onBlur} + onMouseEnter={onHoverIn} + onMouseLeave={onHoverOut} + accessibilityRole="link" + href={href} + {...web({ + hrefAttrs: { + target: download ? undefined : isExternal ? 'blank' : undefined, + rel: isExternal ? 'noopener noreferrer' : undefined, + download, + }, + dataSet: { + // default to no underline, apply this ourselves + noUnderline: '1', + }, + })}> + {children} + </Text> ) } diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index 2c79d27cf..411679102 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -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> diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx index 068ee99e0..c72fcabdd 100644 --- a/src/components/RichText.tsx +++ b/src/components/RichText.tsx @@ -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 & { - value: RichTextAPI | string - testID?: string - numberOfLines?: number - disableLinks?: boolean - resolveFacets?: boolean -}) { + 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} diff --git a/src/components/Typography.tsx b/src/components/Typography.tsx index b34f51018..c9ab7a8a1 100644 --- a/src/components/Typography.tsx +++ b/src/components/Typography.tsx @@ -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)]} /> ) } diff --git a/src/components/icons/Times.tsx b/src/components/icons/Times.tsx new file mode 100644 index 000000000..678ac3fcb --- /dev/null +++ b/src/components/icons/Times.tsx @@ -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', +}) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index aec8338d0..c8e5273d4 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -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_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 function IS_PROD_SERVICE(url?: string) { + return url && url !== STAGING_SERVICE && url !== LOCAL_DEV_SERVICE } -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/' diff --git a/src/lib/strings/embed-player.ts b/src/lib/strings/embed-player.ts index 21a575b91..1cf3b1293 100644 --- a/src/lib/strings/embed-player.ts +++ b/src/lib/strings/embed-player.ts @@ -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} } } diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index 8a71718c8..ef341154d 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -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 diff --git a/src/screens/Onboarding/StepTopicalFeeds.tsx b/src/screens/Onboarding/StepTopicalFeeds.tsx index 4a2210853..636565e34 100644 --- a/src/screens/Onboarding/StepTopicalFeeds.tsx +++ b/src/screens/Onboarding/StepTopicalFeeds.tsx @@ -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( diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index b91db9237..405d054d4 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -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, diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 320009089..40399395a 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -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, diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts index ba4243163..26d40599c 100644 --- a/src/state/queries/post-thread.ts +++ b/src/state/queries/post-thread.ts @@ -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 - } else { - return result.value +): 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 { + 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> { diff --git a/src/state/queries/util.ts b/src/state/queries/util.ts index f3a87ae5d..54752b332 100644 --- a/src/state/queries/util.ts +++ b/src/state/queries/util.ts @@ -53,5 +53,6 @@ export function embedViewRecordToPostView( record: v.value, indexedAt: v.indexedAt, labels: v.labels, + embed: v.embeds?.[0], } } diff --git a/src/view/com/auth/create/state.ts b/src/view/com/auth/create/state.ts index 276eaf924..68cfaceec 100644 --- a/src/view/com/auth/create/state.ts +++ b/src/view/com/auth/create/state.ts @@ -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) { diff --git a/src/view/com/auth/server-input/index.tsx b/src/view/com/auth/server-input/index.tsx index a70621973..32b5a3141 100644 --- a/src/view/com/auth/server-input/index.tsx +++ b/src/view/com/auth/server-input/index.tsx @@ -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 diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx index 0de88b248..9bd7238df 100644 --- a/src/view/com/feeds/FeedSourceCard.tsx +++ b/src/view/com/feeds/FeedSourceCard.tsx @@ -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} diff --git a/src/view/com/lists/ListCard.tsx b/src/view/com/lists/ListCard.tsx index 5750faec1..19842eb54 100644 --- a/src/view/com/lists/ListCard.tsx +++ b/src/view/com/lists/ListCard.tsx @@ -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} diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index f9bdaebed..2ef1b1447 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -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> diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 2f1c0d37b..aec916adb 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -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} diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 8d0f2bef2..6f64de181 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -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} diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index 8fd50fad6..3e479d7b5 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -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} diff --git a/src/view/com/util/EventStopper.tsx b/src/view/com/util/EventStopper.tsx index 1e672e945..e743e89bb 100644 --- a/src/view/com/util/EventStopper.tsx +++ b/src/view/com/util/EventStopper.tsx @@ -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> ) diff --git a/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx b/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx index d556e7669..cf2db5b33 100644 --- a/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx +++ b/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx @@ -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,25 +89,21 @@ function Player({ if (!isPlayerActive) return null return ( - <View style={[styles.layer, styles.playerLayer]}> - <EventStopper> - <View style={{height, width: '100%'}}> - <WebView - javaScriptEnabled={true} - onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} - mediaPlaybackRequiresUserAction={false} - allowsInlineMediaPlayback - bounces={false} - allowsFullscreenVideo - nestedScrollEnabled - source={{uri: params.playerUri}} - onLoad={onLoad} - setSupportMultipleWindows={false} // Prevent any redirects from opening a new window (ads) - style={[styles.webview, styles.topRadius]} - /> - </View> - </EventStopper> - </View> + <EventStopper style={[styles.layer, styles.playerLayer]}> + <WebView + javaScriptEnabled={true} + onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} + mediaPlaybackRequiresUserAction={false} + allowsInlineMediaPlayback + bounces={false} + allowsFullscreenVideo + nestedScrollEnabled + source={{uri: params.playerUri}} + onLoad={onLoad} + style={styles.webview} + setSupportMultipleWindows={false} // Prevent any redirects from opening a new window (ads) + /> + </EventStopper> ) } @@ -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 viewRef = useAnimatedRef() + 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> ) } diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx index d9d84feb4..c128a6f00 100644 --- a/src/view/com/util/post-embeds/QuoteEmbed.tsx +++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx @@ -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={{}} />} diff --git a/src/view/com/util/text/RichText.tsx b/src/view/com/util/text/RichText.tsx index e910127fe..b6d461224 100644 --- a/src/view/com/util/text/RichText.tsx +++ b/src/view/com/util/text/RichText.tsx @@ -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', diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index be9eec816..212c10e74 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -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]}> diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index 796464883..d86b569e2 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -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 diff --git a/src/view/screens/Settings/ExportCarDialog.tsx b/src/view/screens/Settings/ExportCarDialog.tsx index 720cd4f09..dca51c0dc 100644 --- a/src/view/screens/Settings/ExportCarDialog.tsx +++ b/src/view/screens/Settings/ExportCarDialog.tsx @@ -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> diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx index db568c6bd..09be124db 100644 --- a/src/view/screens/Storybook/Dialogs.tsx +++ b/src/view/screens/Storybook/Dialogs.tsx @@ -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> diff --git a/src/view/screens/Storybook/Typography.tsx b/src/view/screens/Storybook/Typography.tsx index 5d3a96f4d..8ee4270b2 100644 --- a/src/view/screens/Storybook/Typography.tsx +++ b/src/view/screens/Storybook/Typography.tsx @@ -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]} |