Merge branch 'main' into hailey/use-eas-version-code
This commit is contained in:
@@ -215,6 +215,7 @@
|
|||||||
<meta name="application-name" content="Bluesky">
|
<meta name="application-name" content="Bluesky">
|
||||||
<meta name="generator" content="bskyweb">
|
<meta name="generator" content="bskyweb">
|
||||||
<meta property="og:site_name" content="Bluesky Social" />
|
<meta property="og:site_name" content="Bluesky Social" />
|
||||||
|
<link type="application/activity+json" href="" />
|
||||||
|
|
||||||
{% block html_head_extra -%}{%- endblock %}
|
{% block html_head_extra -%}{%- endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
+7
-5
@@ -21,7 +21,6 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
|||||||
import Svg, {Path, SvgProps} from 'react-native-svg'
|
import Svg, {Path, SvgProps} from 'react-native-svg'
|
||||||
|
|
||||||
import {isAndroid} from '#/platform/detection'
|
import {isAndroid} from '#/platform/detection'
|
||||||
import {useThemePrefs} from 'state/shell'
|
|
||||||
import {Logotype} from '#/view/icons/Logotype'
|
import {Logotype} from '#/view/icons/Logotype'
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -75,10 +74,8 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
|||||||
isLayoutReady &&
|
isLayoutReady &&
|
||||||
reduceMotion !== undefined
|
reduceMotion !== undefined
|
||||||
|
|
||||||
const {colorMode} = useThemePrefs()
|
|
||||||
const colorScheme = useColorScheme()
|
const colorScheme = useColorScheme()
|
||||||
const themeName = colorMode === 'system' ? colorScheme : colorMode
|
const isDarkMode = colorScheme === 'dark'
|
||||||
const isDarkMode = themeName === 'dark'
|
|
||||||
|
|
||||||
const logoAnimation = useAnimatedStyle(() => {
|
const logoAnimation = useAnimatedStyle(() => {
|
||||||
return {
|
return {
|
||||||
@@ -263,7 +260,12 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
StyleSheet.absoluteFillObject,
|
StyleSheet.absoluteFillObject,
|
||||||
{backgroundColor: '#fff'},
|
{
|
||||||
|
backgroundColor: isDarkMode
|
||||||
|
? // special off-spec color for dark mode
|
||||||
|
'#0F1824'
|
||||||
|
: '#fff',
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -122,6 +122,9 @@ export const atoms = {
|
|||||||
flex_shrink: {
|
flex_shrink: {
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
},
|
},
|
||||||
|
justify_start: {
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
},
|
||||||
justify_center: {
|
justify_center: {
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
@@ -140,10 +143,31 @@ export const atoms = {
|
|||||||
align_end: {
|
align_end: {
|
||||||
alignItems: 'flex-end',
|
alignItems: 'flex-end',
|
||||||
},
|
},
|
||||||
|
self_auto: {
|
||||||
|
alignSelf: 'auto',
|
||||||
|
},
|
||||||
|
self_start: {
|
||||||
|
alignSelf: 'flex-start',
|
||||||
|
},
|
||||||
|
self_end: {
|
||||||
|
alignSelf: 'flex-end',
|
||||||
|
},
|
||||||
|
self_center: {
|
||||||
|
alignSelf: 'center',
|
||||||
|
},
|
||||||
|
self_stretch: {
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
},
|
||||||
|
self_baseline: {
|
||||||
|
alignSelf: 'baseline',
|
||||||
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Text
|
* Text
|
||||||
*/
|
*/
|
||||||
|
text_left: {
|
||||||
|
textAlign: 'left',
|
||||||
|
},
|
||||||
text_center: {
|
text_center: {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
@@ -195,10 +219,16 @@ export const atoms = {
|
|||||||
font_bold: {
|
font_bold: {
|
||||||
fontWeight: tokens.fontWeight.semibold,
|
fontWeight: tokens.fontWeight.semibold,
|
||||||
},
|
},
|
||||||
|
italic: {
|
||||||
|
fontStyle: 'italic',
|
||||||
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Border
|
* Border
|
||||||
*/
|
*/
|
||||||
|
border_0: {
|
||||||
|
borderWidth: 0,
|
||||||
|
},
|
||||||
border: {
|
border: {
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
},
|
},
|
||||||
@@ -208,6 +238,12 @@ export const atoms = {
|
|||||||
border_b: {
|
border_b: {
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
},
|
},
|
||||||
|
border_l: {
|
||||||
|
borderLeftWidth: 1,
|
||||||
|
},
|
||||||
|
border_r: {
|
||||||
|
borderRightWidth: 1,
|
||||||
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Shadow
|
* Shadow
|
||||||
|
|||||||
+67
-35
@@ -27,7 +27,7 @@ export type ButtonColor =
|
|||||||
| 'gradient_sunset'
|
| 'gradient_sunset'
|
||||||
| 'gradient_nordic'
|
| 'gradient_nordic'
|
||||||
| 'gradient_bonfire'
|
| 'gradient_bonfire'
|
||||||
export type ButtonSize = 'small' | 'large'
|
export type ButtonSize = 'tiny' | 'small' | 'large'
|
||||||
export type ButtonShape = 'round' | 'square' | 'default'
|
export type ButtonShape = 'round' | 'square' | 'default'
|
||||||
export type VariantProps = {
|
export type VariantProps = {
|
||||||
/**
|
/**
|
||||||
@@ -48,25 +48,32 @@ export type VariantProps = {
|
|||||||
shape?: ButtonShape
|
shape?: ButtonShape
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ButtonProps = React.PropsWithChildren<
|
export type ButtonState = {
|
||||||
Pick<PressableProps, 'disabled' | 'onPress'> &
|
hovered: boolean
|
||||||
AccessibilityProps &
|
focused: boolean
|
||||||
VariantProps & {
|
pressed: boolean
|
||||||
testID?: string
|
disabled: boolean
|
||||||
label: string
|
}
|
||||||
style?: StyleProp<ViewStyle>
|
|
||||||
}
|
export type ButtonContext = VariantProps & ButtonState
|
||||||
>
|
|
||||||
|
export type ButtonProps = Pick<
|
||||||
|
PressableProps,
|
||||||
|
'disabled' | 'onPress' | 'testID'
|
||||||
|
> &
|
||||||
|
AccessibilityProps &
|
||||||
|
VariantProps & {
|
||||||
|
testID?: string
|
||||||
|
label: string
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
|
children:
|
||||||
|
| React.ReactNode
|
||||||
|
| string
|
||||||
|
| ((context: ButtonContext) => React.ReactNode | string)
|
||||||
|
}
|
||||||
export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
|
export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
|
||||||
|
|
||||||
const Context = React.createContext<
|
const Context = React.createContext<VariantProps & ButtonState>({
|
||||||
VariantProps & {
|
|
||||||
hovered: boolean
|
|
||||||
focused: boolean
|
|
||||||
pressed: boolean
|
|
||||||
disabled: boolean
|
|
||||||
}
|
|
||||||
>({
|
|
||||||
hovered: false,
|
hovered: false,
|
||||||
focused: false,
|
focused: false,
|
||||||
pressed: false,
|
pressed: false,
|
||||||
@@ -277,6 +284,8 @@ export function Button({
|
|||||||
baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md)
|
baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md)
|
||||||
} else if (size === 'small') {
|
} else if (size === 'small') {
|
||||||
baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm)
|
baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm)
|
||||||
|
} else if (size === 'tiny') {
|
||||||
|
baseStyles.push({paddingVertical: 4}, a.px_sm, a.rounded_xs, a.gap_xs)
|
||||||
}
|
}
|
||||||
} else if (shape === 'round' || shape === 'square') {
|
} else if (shape === 'round' || shape === 'square') {
|
||||||
if (size === 'large') {
|
if (size === 'large') {
|
||||||
@@ -287,12 +296,18 @@ export function Button({
|
|||||||
}
|
}
|
||||||
} else if (size === 'small') {
|
} else if (size === 'small') {
|
||||||
baseStyles.push({height: 40, width: 40})
|
baseStyles.push({height: 40, width: 40})
|
||||||
|
} else if (size === 'tiny') {
|
||||||
|
baseStyles.push({height: 20, width: 20})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shape === 'round') {
|
if (shape === 'round') {
|
||||||
baseStyles.push(a.rounded_full)
|
baseStyles.push(a.rounded_full)
|
||||||
} else if (shape === 'square') {
|
} else if (shape === 'square') {
|
||||||
baseStyles.push(a.rounded_sm)
|
if (size === 'tiny') {
|
||||||
|
baseStyles.push(a.rounded_xs)
|
||||||
|
} else {
|
||||||
|
baseStyles.push(a.rounded_sm)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,7 +353,7 @@ export function Button({
|
|||||||
}
|
}
|
||||||
}, [variant, color])
|
}, [variant, color])
|
||||||
|
|
||||||
const context = React.useMemo(
|
const context = React.useMemo<ButtonContext>(
|
||||||
() => ({
|
() => ({
|
||||||
...state,
|
...state,
|
||||||
variant,
|
variant,
|
||||||
@@ -349,6 +364,8 @@ export function Button({
|
|||||||
[state, variant, color, size, disabled],
|
[state, variant, color, size, disabled],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const flattenedBaseStyles = flatten(baseStyles)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
role="button"
|
role="button"
|
||||||
@@ -362,15 +379,14 @@ export function Button({
|
|||||||
disabled: disabled || false,
|
disabled: disabled || false,
|
||||||
}}
|
}}
|
||||||
style={[
|
style={[
|
||||||
flatten(style),
|
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
a.justify_center,
|
a.justify_center,
|
||||||
a.overflow_hidden,
|
|
||||||
a.justify_center,
|
a.justify_center,
|
||||||
...baseStyles,
|
flattenedBaseStyles,
|
||||||
...(state.hovered || state.pressed ? hoverStyles : []),
|
...(state.hovered || state.pressed ? hoverStyles : []),
|
||||||
...(state.focused ? focusStyles : []),
|
...(state.focused ? focusStyles : []),
|
||||||
|
flatten(style),
|
||||||
]}
|
]}
|
||||||
onPressIn={onPressIn}
|
onPressIn={onPressIn}
|
||||||
onPressOut={onPressOut}
|
onPressOut={onPressOut}
|
||||||
@@ -379,21 +395,31 @@ export function Button({
|
|||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
onBlur={onBlur}>
|
onBlur={onBlur}>
|
||||||
{variant === 'gradient' && (
|
{variant === 'gradient' && (
|
||||||
<LinearGradient
|
<View
|
||||||
colors={
|
style={[
|
||||||
state.hovered || state.pressed || state.focused
|
a.absolute,
|
||||||
? gradientHoverColors
|
a.inset_0,
|
||||||
: gradientColors
|
a.overflow_hidden,
|
||||||
}
|
{borderRadius: flattenedBaseStyles.borderRadius},
|
||||||
locations={gradientLocations}
|
]}>
|
||||||
start={{x: 0, y: 0}}
|
<LinearGradient
|
||||||
end={{x: 1, y: 1}}
|
colors={
|
||||||
style={[a.absolute, a.inset_0]}
|
state.hovered || state.pressed || state.focused
|
||||||
/>
|
? gradientHoverColors
|
||||||
|
: gradientColors
|
||||||
|
}
|
||||||
|
locations={gradientLocations}
|
||||||
|
start={{x: 0, y: 0}}
|
||||||
|
end={{x: 1, y: 1}}
|
||||||
|
style={[a.absolute, a.inset_0]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
<Context.Provider value={context}>
|
<Context.Provider value={context}>
|
||||||
{typeof children === 'string' ? (
|
{typeof children === 'string' ? (
|
||||||
<ButtonText>{children}</ButtonText>
|
<ButtonText>{children}</ButtonText>
|
||||||
|
) : typeof children === 'function' ? (
|
||||||
|
children(context)
|
||||||
) : (
|
) : (
|
||||||
children
|
children
|
||||||
)}
|
)}
|
||||||
@@ -493,6 +519,8 @@ export function useSharedButtonTextStyles() {
|
|||||||
|
|
||||||
if (size === 'large') {
|
if (size === 'large') {
|
||||||
baseStyles.push(a.text_md, android({paddingBottom: 1}))
|
baseStyles.push(a.text_md, android({paddingBottom: 1}))
|
||||||
|
} else if (size === 'tiny') {
|
||||||
|
baseStyles.push(a.text_xs, android({paddingBottom: 1}))
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push(a.text_sm, android({paddingBottom: 1}))
|
baseStyles.push(a.text_sm, android({paddingBottom: 1}))
|
||||||
}
|
}
|
||||||
@@ -514,9 +542,11 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
|
|||||||
export function ButtonIcon({
|
export function ButtonIcon({
|
||||||
icon: Comp,
|
icon: Comp,
|
||||||
position,
|
position,
|
||||||
|
size: iconSize,
|
||||||
}: {
|
}: {
|
||||||
icon: React.ComponentType<SVGIconProps>
|
icon: React.ComponentType<SVGIconProps>
|
||||||
position?: 'left' | 'right'
|
position?: 'left' | 'right'
|
||||||
|
size?: SVGIconProps['size']
|
||||||
}) {
|
}) {
|
||||||
const {size, disabled} = useButtonContext()
|
const {size, disabled} = useButtonContext()
|
||||||
const textStyles = useSharedButtonTextStyles()
|
const textStyles = useSharedButtonTextStyles()
|
||||||
@@ -532,7 +562,9 @@ export function ButtonIcon({
|
|||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
<Comp
|
<Comp
|
||||||
size={size === 'large' ? 'md' : 'sm'}
|
size={
|
||||||
|
iconSize ?? (size === 'large' ? 'md' : size === 'tiny' ? 'xs' : 'sm')
|
||||||
|
}
|
||||||
style={[{color: textStyles.color, pointerEvents: 'none'}]}
|
style={[{color: textStyles.color, pointerEvents: 'none'}]}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
+53
-32
@@ -13,7 +13,7 @@ import {sanitizeUrl} from '@braintree/sanitize-url'
|
|||||||
|
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {useTheme, web, flatten, TextStyleProp} from '#/alf'
|
import {useTheme, web, flatten, TextStyleProp, atoms as a} from '#/alf'
|
||||||
import {Button, ButtonProps} from '#/components/Button'
|
import {Button, ButtonProps} from '#/components/Button'
|
||||||
import {AllNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
import {AllNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
||||||
import {
|
import {
|
||||||
@@ -35,6 +35,13 @@ type BaseLinkProps = Pick<
|
|||||||
Parameters<typeof useLinkProps<AllNavigatorParams>>[0],
|
Parameters<typeof useLinkProps<AllNavigatorParams>>[0],
|
||||||
'to'
|
'to'
|
||||||
> & {
|
> & {
|
||||||
|
testID?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Label for a11y. Defaults to the href.
|
||||||
|
*/
|
||||||
|
label?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The React Navigation `StackAction` to perform when the link is pressed.
|
* The React Navigation `StackAction` to perform when the link is pressed.
|
||||||
*/
|
*/
|
||||||
@@ -46,6 +53,18 @@ type BaseLinkProps = Pick<
|
|||||||
* Note: atm this only works for `InlineLink`s with a string child.
|
* Note: atm this only works for `InlineLink`s with a string child.
|
||||||
*/
|
*/
|
||||||
warnOnMismatchingTextChild?: boolean
|
warnOnMismatchingTextChild?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for when the link is pressed.
|
||||||
|
*
|
||||||
|
* DO NOT use this for navigation, that's what the `to` prop is for.
|
||||||
|
*/
|
||||||
|
onPress?: (e: GestureResponderEvent) => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web-only attribute. Sets `download` attr on web.
|
||||||
|
*/
|
||||||
|
download?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLink({
|
export function useLink({
|
||||||
@@ -53,6 +72,7 @@ export function useLink({
|
|||||||
displayText,
|
displayText,
|
||||||
action = 'push',
|
action = 'push',
|
||||||
warnOnMismatchingTextChild,
|
warnOnMismatchingTextChild,
|
||||||
|
onPress: outerOnPress,
|
||||||
}: BaseLinkProps & {
|
}: BaseLinkProps & {
|
||||||
displayText: string
|
displayText: string
|
||||||
}) {
|
}) {
|
||||||
@@ -66,6 +86,8 @@ export function useLink({
|
|||||||
|
|
||||||
const onPress = React.useCallback(
|
const onPress = React.useCallback(
|
||||||
(e: GestureResponderEvent) => {
|
(e: GestureResponderEvent) => {
|
||||||
|
outerOnPress?.(e)
|
||||||
|
|
||||||
const requiresWarning = Boolean(
|
const requiresWarning = Boolean(
|
||||||
warnOnMismatchingTextChild &&
|
warnOnMismatchingTextChild &&
|
||||||
displayText &&
|
displayText &&
|
||||||
@@ -132,6 +154,7 @@ export function useLink({
|
|||||||
displayText,
|
displayText,
|
||||||
closeModal,
|
closeModal,
|
||||||
openModal,
|
openModal,
|
||||||
|
outerOnPress,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -143,16 +166,7 @@ export function useLink({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type LinkProps = Omit<BaseLinkProps, 'warnOnMismatchingTextChild'> &
|
export type LinkProps = Omit<BaseLinkProps, 'warnOnMismatchingTextChild'> &
|
||||||
Omit<ButtonProps, 'style' | 'onPress' | 'disabled' | 'label'> & {
|
Omit<ButtonProps, 'onPress' | 'disabled' | 'label'>
|
||||||
/**
|
|
||||||
* Label for a11y. Defaults to the href.
|
|
||||||
*/
|
|
||||||
label?: string
|
|
||||||
/**
|
|
||||||
* Web-only attribute. Sets `download` attr on web.
|
|
||||||
*/
|
|
||||||
download?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A interactive element that renders as a `<a>` tag on the web. On mobile it
|
* A interactive element that renders as a `<a>` tag on the web. On mobile it
|
||||||
@@ -166,6 +180,7 @@ export function Link({
|
|||||||
children,
|
children,
|
||||||
to,
|
to,
|
||||||
action = 'push',
|
action = 'push',
|
||||||
|
onPress: outerOnPress,
|
||||||
download,
|
download,
|
||||||
...rest
|
...rest
|
||||||
}: LinkProps) {
|
}: LinkProps) {
|
||||||
@@ -173,24 +188,26 @@ export function Link({
|
|||||||
to,
|
to,
|
||||||
displayText: typeof children === 'string' ? children : '',
|
displayText: typeof children === 'string' ? children : '',
|
||||||
action,
|
action,
|
||||||
|
onPress: outerOnPress,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
label={href}
|
label={href}
|
||||||
{...rest}
|
{...rest}
|
||||||
|
style={[a.justify_start, flatten(rest.style)]}
|
||||||
role="link"
|
role="link"
|
||||||
accessibilityRole="link"
|
accessibilityRole="link"
|
||||||
href={href}
|
href={href}
|
||||||
onPress={onPress}
|
onPress={download ? undefined : onPress}
|
||||||
{...web({
|
{...web({
|
||||||
hrefAttrs: {
|
hrefAttrs: {
|
||||||
target: isExternal ? 'blank' : undefined,
|
target: download ? undefined : isExternal ? 'blank' : undefined,
|
||||||
rel: isExternal ? 'noopener noreferrer' : undefined,
|
rel: isExternal ? 'noopener noreferrer' : undefined,
|
||||||
download,
|
download,
|
||||||
},
|
},
|
||||||
dataSet: {
|
dataSet: {
|
||||||
// default to no underline, apply this ourselves
|
// no underline, only `InlineLink` has underlines
|
||||||
noUnderline: '1',
|
noUnderline: '1',
|
||||||
},
|
},
|
||||||
})}>
|
})}>
|
||||||
@@ -200,13 +217,7 @@ export function Link({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type InlineLinkProps = React.PropsWithChildren<
|
export type InlineLinkProps = React.PropsWithChildren<
|
||||||
BaseLinkProps &
|
BaseLinkProps & TextStyleProp
|
||||||
TextStyleProp & {
|
|
||||||
/**
|
|
||||||
* Label for a11y. Defaults to the href.
|
|
||||||
*/
|
|
||||||
label?: string
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
|
|
||||||
export function InlineLink({
|
export function InlineLink({
|
||||||
@@ -215,6 +226,8 @@ export function InlineLink({
|
|||||||
action = 'push',
|
action = 'push',
|
||||||
warnOnMismatchingTextChild,
|
warnOnMismatchingTextChild,
|
||||||
style,
|
style,
|
||||||
|
onPress: outerOnPress,
|
||||||
|
download,
|
||||||
...rest
|
...rest
|
||||||
}: InlineLinkProps) {
|
}: InlineLinkProps) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -224,18 +237,25 @@ export function InlineLink({
|
|||||||
displayText: stringChildren ? children : '',
|
displayText: stringChildren ? children : '',
|
||||||
action,
|
action,
|
||||||
warnOnMismatchingTextChild,
|
warnOnMismatchingTextChild,
|
||||||
|
onPress: outerOnPress,
|
||||||
})
|
})
|
||||||
|
const {
|
||||||
|
state: hovered,
|
||||||
|
onIn: onHoverIn,
|
||||||
|
onOut: onHoverOut,
|
||||||
|
} = useInteractionState()
|
||||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||||
const {
|
const {
|
||||||
state: pressed,
|
state: pressed,
|
||||||
onIn: onPressIn,
|
onIn: onPressIn,
|
||||||
onOut: onPressOut,
|
onOut: onPressOut,
|
||||||
} = useInteractionState()
|
} = useInteractionState()
|
||||||
|
const flattenedStyle = flatten(style)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
onPress={onPress}
|
onPress={download ? undefined : onPress}
|
||||||
onPressIn={onPressIn}
|
onPressIn={onPressIn}
|
||||||
onPressOut={onPressOut}
|
onPressOut={onPressOut}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
@@ -245,27 +265,28 @@ export function InlineLink({
|
|||||||
{...rest}
|
{...rest}
|
||||||
style={[
|
style={[
|
||||||
{color: t.palette.primary_500},
|
{color: t.palette.primary_500},
|
||||||
(focused || pressed) && {
|
(hovered || focused || pressed) && {
|
||||||
outline: 0,
|
outline: 0,
|
||||||
textDecorationLine: 'underline',
|
textDecorationLine: 'underline',
|
||||||
textDecorationColor: t.palette.primary_500,
|
textDecorationColor: flattenedStyle.color ?? t.palette.primary_500,
|
||||||
},
|
},
|
||||||
flatten(style),
|
flattenedStyle,
|
||||||
]}
|
]}
|
||||||
role="link"
|
role="link"
|
||||||
|
onMouseEnter={onHoverIn}
|
||||||
|
onMouseLeave={onHoverOut}
|
||||||
accessibilityRole="link"
|
accessibilityRole="link"
|
||||||
href={href}
|
href={href}
|
||||||
{...web({
|
{...web({
|
||||||
hrefAttrs: {
|
hrefAttrs: {
|
||||||
target: isExternal ? 'blank' : undefined,
|
target: download ? undefined : isExternal ? 'blank' : undefined,
|
||||||
rel: isExternal ? 'noopener noreferrer' : undefined,
|
rel: isExternal ? 'noopener noreferrer' : undefined,
|
||||||
|
download,
|
||||||
|
},
|
||||||
|
dataSet: {
|
||||||
|
// default to no underline, apply this ourselves
|
||||||
|
noUnderline: '1',
|
||||||
},
|
},
|
||||||
dataSet: stringChildren
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
// default to no underline, apply this ourselves
|
|
||||||
noUnderline: '1',
|
|
||||||
},
|
|
||||||
})}>
|
})}>
|
||||||
{children}
|
{children}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ import Animated, {
|
|||||||
withTiming,
|
withTiming,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
|
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a, useTheme, flatten} from '#/alf'
|
||||||
import {Props, useCommonSVGProps} from '#/components/icons/common'
|
import {Props, useCommonSVGProps} from '#/components/icons/common'
|
||||||
import {Loader_Stroke2_Corner0_Rounded as Icon} from '#/components/icons/Loader'
|
import {Loader_Stroke2_Corner0_Rounded as Icon} from '#/components/icons/Loader'
|
||||||
|
|
||||||
export function Loader(props: Props) {
|
export function Loader(props: Props) {
|
||||||
|
const t = useTheme()
|
||||||
const common = useCommonSVGProps(props)
|
const common = useCommonSVGProps(props)
|
||||||
const rotation = useSharedValue(0)
|
const rotation = useSharedValue(0)
|
||||||
|
|
||||||
@@ -35,7 +36,15 @@ export function Loader(props: Props) {
|
|||||||
{width: common.size, height: common.size},
|
{width: common.size, height: common.size},
|
||||||
animatedStyles,
|
animatedStyles,
|
||||||
]}>
|
]}>
|
||||||
<Icon {...props} style={[a.absolute, a.inset_0, props.style]} />
|
<Icon
|
||||||
|
{...props}
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.inset_0,
|
||||||
|
t.atoms.text_contrast_high,
|
||||||
|
flatten(props.style),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,6 +344,25 @@ export const dimTheme: Theme = {
|
|||||||
default: {
|
default: {
|
||||||
...darkTheme.palette.default,
|
...darkTheme.palette.default,
|
||||||
background: dimPalette.black,
|
background: dimPalette.black,
|
||||||
|
backgroundLight: dimPalette.contrast_50,
|
||||||
|
text: dimPalette.white,
|
||||||
|
textLight: dimPalette.contrast_700,
|
||||||
|
textInverted: dimPalette.black,
|
||||||
|
link: dimPalette.primary_500,
|
||||||
|
border: dimPalette.contrast_100,
|
||||||
|
borderDark: dimPalette.contrast_200,
|
||||||
|
icon: dimPalette.contrast_500,
|
||||||
|
|
||||||
|
// non-standard
|
||||||
|
textVeryLight: dimPalette.contrast_400,
|
||||||
|
replyLine: dimPalette.contrast_100,
|
||||||
|
replyLineDot: dimPalette.contrast_200,
|
||||||
|
unreadNotifBg: dimPalette.primary_975,
|
||||||
|
unreadNotifBorder: dimPalette.primary_900,
|
||||||
|
postCtrl: dimPalette.contrast_500,
|
||||||
|
brandText: dimPalette.primary_500,
|
||||||
|
emptyStateIcon: dimPalette.contrast_300,
|
||||||
|
borderLinkHover: dimPalette.contrast_300,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {networkRetry} from '#/lib/async/retry'
|
|||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import * as persisted from '#/state/persisted'
|
import * as persisted from '#/state/persisted'
|
||||||
import {PUBLIC_BSKY_AGENT} from '#/state/queries'
|
import {PUBLIC_BSKY_AGENT} from '#/state/queries'
|
||||||
import {IS_PROD} from '#/lib/constants'
|
|
||||||
import {emitSessionDropped} from '../events'
|
import {emitSessionDropped} from '../events'
|
||||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||||
import {useCloseAllActiveElements} from '#/state/util'
|
import {useCloseAllActiveElements} from '#/state/util'
|
||||||
@@ -36,7 +35,6 @@ export type SessionState = {
|
|||||||
}
|
}
|
||||||
export type StateContext = SessionState & {
|
export type StateContext = SessionState & {
|
||||||
hasSession: boolean
|
hasSession: boolean
|
||||||
isSandbox: boolean
|
|
||||||
}
|
}
|
||||||
export type ApiContext = {
|
export type ApiContext = {
|
||||||
createAccount: (props: {
|
createAccount: (props: {
|
||||||
@@ -84,7 +82,6 @@ const StateContext = React.createContext<StateContext>({
|
|||||||
accounts: [],
|
accounts: [],
|
||||||
currentAccount: undefined,
|
currentAccount: undefined,
|
||||||
hasSession: false,
|
hasSession: false,
|
||||||
isSandbox: false,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const ApiContext = React.createContext<ApiContext>({
|
const ApiContext = React.createContext<ApiContext>({
|
||||||
@@ -610,9 +607,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
() => ({
|
() => ({
|
||||||
...state,
|
...state,
|
||||||
hasSession: !!state.currentAccount,
|
hasSession: !!state.currentAccount,
|
||||||
isSandbox: state.currentAccount
|
|
||||||
? !IS_PROD(state.currentAccount?.service)
|
|
||||||
: false,
|
|
||||||
}),
|
}),
|
||||||
[state],
|
[state],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function FeedPage({
|
|||||||
renderEmptyState: () => JSX.Element
|
renderEmptyState: () => JSX.Element
|
||||||
renderEndOfFeed?: () => JSX.Element
|
renderEndOfFeed?: () => JSX.Element
|
||||||
}) {
|
}) {
|
||||||
const {isSandbox, hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
@@ -119,7 +119,7 @@ export function FeedPage({
|
|||||||
style={[pal.text, {fontWeight: 'bold'}]}
|
style={[pal.text, {fontWeight: 'bold'}]}
|
||||||
text={
|
text={
|
||||||
<>
|
<>
|
||||||
{isSandbox ? 'SANDBOX' : 'Bluesky'}{' '}
|
Bluesky{' '}
|
||||||
{hasNew && (
|
{hasNew && (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@@ -154,16 +154,7 @@ export function FeedPage({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
return <></>
|
return <></>
|
||||||
}, [
|
}, [isDesktop, pal.view, pal.text, pal.textLight, hasNew, _, hasSession])
|
||||||
isDesktop,
|
|
||||||
pal.view,
|
|
||||||
pal.text,
|
|
||||||
pal.textLight,
|
|
||||||
hasNew,
|
|
||||||
_,
|
|
||||||
isSandbox,
|
|
||||||
hasSession,
|
|
||||||
])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View testID={testID} style={s.h100pct}>
|
<View testID={testID} style={s.h100pct}>
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export function FeedsTabBar(
|
|||||||
|
|
||||||
function FeedsTabBarPublic() {
|
function FeedsTabBarPublic() {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {isSandbox} = useSession()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredView sideBorders>
|
<CenteredView sideBorders>
|
||||||
@@ -56,23 +55,7 @@ function FeedsTabBarPublic() {
|
|||||||
type="title-lg"
|
type="title-lg"
|
||||||
href="/"
|
href="/"
|
||||||
style={[pal.text, {fontWeight: 'bold'}]}
|
style={[pal.text, {fontWeight: 'bold'}]}
|
||||||
text={
|
text="Bluesky "
|
||||||
<>
|
|
||||||
{isSandbox ? 'SANDBOX' : 'Bluesky'}{' '}
|
|
||||||
{/*hasNew && (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
top: -8,
|
|
||||||
backgroundColor: colors.blue3,
|
|
||||||
width: 8,
|
|
||||||
height: 8,
|
|
||||||
borderRadius: 4,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)*/}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
// onPress={emitSoftReset}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
|
|||||||
@@ -43,10 +43,13 @@ import {
|
|||||||
usePreferencesQuery,
|
usePreferencesQuery,
|
||||||
} from '#/state/queries/preferences'
|
} from '#/state/queries/preferences'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {isAndroid, isNative} from '#/platform/detection'
|
import {isAndroid, isNative, isWeb} from '#/platform/detection'
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||||
|
|
||||||
|
// FlatList maintainVisibleContentPosition breaks if too many items
|
||||||
|
// are prepended. This seems to be an optimal number based on *shrug*.
|
||||||
|
const PARENTS_CHUNK_SIZE = 15
|
||||||
|
|
||||||
const MAINTAIN_VISIBLE_CONTENT_POSITION = {
|
const MAINTAIN_VISIBLE_CONTENT_POSITION = {
|
||||||
// We don't insert any elements before the root row while loading.
|
// We don't insert any elements before the root row while loading.
|
||||||
// So the row we want to use as the scroll anchor is the first row.
|
// So the row we want to use as the scroll anchor is the first row.
|
||||||
@@ -165,8 +168,10 @@ function PostThreadLoaded({
|
|||||||
const {isMobile, isTabletOrMobile} = useWebMediaQueries()
|
const {isMobile, isTabletOrMobile} = useWebMediaQueries()
|
||||||
const ref = useRef<ListMethods>(null)
|
const ref = useRef<ListMethods>(null)
|
||||||
const highlightedPostRef = useRef<View | null>(null)
|
const highlightedPostRef = useRef<View | null>(null)
|
||||||
const [maxVisible, setMaxVisible] = React.useState(100)
|
const [maxParents, setMaxParents] = React.useState(
|
||||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
isWeb ? Infinity : PARENTS_CHUNK_SIZE,
|
||||||
|
)
|
||||||
|
const [maxReplies, setMaxReplies] = React.useState(100)
|
||||||
const treeView = React.useMemo(
|
const treeView = React.useMemo(
|
||||||
() => !!threadViewPrefs.lab_treeViewEnabled && hasBranchingReplies(thread),
|
() => !!threadViewPrefs.lab_treeViewEnabled && hasBranchingReplies(thread),
|
||||||
[threadViewPrefs, thread],
|
[threadViewPrefs, thread],
|
||||||
@@ -206,10 +211,18 @@ function PostThreadLoaded({
|
|||||||
// maintainVisibleContentPosition and onContentSizeChange
|
// maintainVisibleContentPosition and onContentSizeChange
|
||||||
// to "hold onto" the correct row instead of the first one.
|
// to "hold onto" the correct row instead of the first one.
|
||||||
} else {
|
} else {
|
||||||
// Everything is loaded.
|
// Everything is loaded
|
||||||
arr.push(TOP_COMPONENT)
|
let startIndex = Math.max(0, parents.length - maxParents)
|
||||||
for (const parent of parents) {
|
if (startIndex === 0) {
|
||||||
arr.push(parent)
|
arr.push(TOP_COMPONENT)
|
||||||
|
} else {
|
||||||
|
// When progressively revealing parents, rendering a placeholder
|
||||||
|
// here will cause scrolling jumps. Don't add it unless you test it.
|
||||||
|
// QT'ing this thread is a great way to test all the scrolling hacks:
|
||||||
|
// https://bsky.app/profile/www.mozzius.dev/post/3kjqhblh6qk2o
|
||||||
|
}
|
||||||
|
for (let i = startIndex; i < parents.length; i++) {
|
||||||
|
arr.push(parents[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,17 +233,18 @@ function PostThreadLoaded({
|
|||||||
if (highlightedPost.ctx.isChildLoading) {
|
if (highlightedPost.ctx.isChildLoading) {
|
||||||
arr.push(CHILD_SPINNER)
|
arr.push(CHILD_SPINNER)
|
||||||
} else {
|
} else {
|
||||||
for (const reply of replies) {
|
for (let i = 0; i < replies.length; i++) {
|
||||||
arr.push(reply)
|
arr.push(replies[i])
|
||||||
|
if (i === maxReplies) {
|
||||||
|
arr.push(LOAD_MORE)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
arr.push(BOTTOM_COMPONENT)
|
arr.push(BOTTOM_COMPONENT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (arr.length > maxVisible) {
|
|
||||||
arr = arr.slice(0, maxVisible).concat([LOAD_MORE])
|
|
||||||
}
|
|
||||||
return arr
|
return arr
|
||||||
}, [skeleton, maxVisible, deferParents])
|
}, [skeleton, deferParents, maxParents, maxReplies])
|
||||||
|
|
||||||
// This is only used on the web to keep the post in view when its parents load.
|
// This is only used on the web to keep the post in view when its parents load.
|
||||||
// On native, we rely on `maintainVisibleContentPosition` instead.
|
// On native, we rely on `maintainVisibleContentPosition` instead.
|
||||||
@@ -258,15 +272,28 @@ function PostThreadLoaded({
|
|||||||
}
|
}
|
||||||
}, [thread])
|
}, [thread])
|
||||||
|
|
||||||
const onPTR = React.useCallback(async () => {
|
// On native, we reveal parents in chunks. Although they're all already
|
||||||
setIsPTRing(true)
|
// loaded and FlatList already has its own virtualization, unfortunately FlatList
|
||||||
try {
|
// has a bug that causes the content to jump around if too many items are getting
|
||||||
await onRefresh()
|
// prepended at once. It also jumps around if items get prepended during scroll.
|
||||||
} catch (err) {
|
// To work around this, we prepend rows after scroll bumps against the top and rests.
|
||||||
logger.error('Failed to refresh posts thread', {message: err})
|
const needsBumpMaxParents = React.useRef(false)
|
||||||
|
const onStartReached = React.useCallback(() => {
|
||||||
|
if (maxParents < skeleton.parents.length) {
|
||||||
|
needsBumpMaxParents.current = true
|
||||||
}
|
}
|
||||||
setIsPTRing(false)
|
}, [maxParents, skeleton.parents.length])
|
||||||
}, [setIsPTRing, onRefresh])
|
const bumpMaxParentsIfNeeded = React.useCallback(() => {
|
||||||
|
if (!isNative) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (needsBumpMaxParents.current) {
|
||||||
|
needsBumpMaxParents.current = false
|
||||||
|
setMaxParents(n => n + PARENTS_CHUNK_SIZE)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
const onMomentumScrollEnd = bumpMaxParentsIfNeeded
|
||||||
|
const onScrollToTop = bumpMaxParentsIfNeeded
|
||||||
|
|
||||||
const renderItem = React.useCallback(
|
const renderItem = React.useCallback(
|
||||||
({item, index}: {item: RowItem; index: number}) => {
|
({item, index}: {item: RowItem; index: number}) => {
|
||||||
@@ -301,7 +328,7 @@ function PostThreadLoaded({
|
|||||||
} else if (item === LOAD_MORE) {
|
} else if (item === LOAD_MORE) {
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => setMaxVisible(n => n + 50)}
|
onPress={() => setMaxReplies(n => n + 50)}
|
||||||
style={[pal.border, pal.view, styles.itemContainer]}
|
style={[pal.border, pal.view, styles.itemContainer]}
|
||||||
accessibilityLabel={_(msg`Load more posts`)}
|
accessibilityLabel={_(msg`Load more posts`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
@@ -345,6 +372,8 @@ function PostThreadLoaded({
|
|||||||
const next = isThreadPost(posts[index - 1])
|
const next = isThreadPost(posts[index - 1])
|
||||||
? (posts[index - 1] as ThreadPost)
|
? (posts[index - 1] as ThreadPost)
|
||||||
: undefined
|
: undefined
|
||||||
|
const hasUnrevealedParents =
|
||||||
|
index === 0 && maxParents < skeleton.parents.length
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}
|
ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}
|
||||||
@@ -360,7 +389,9 @@ function PostThreadLoaded({
|
|||||||
hasMore={item.ctx.hasMore}
|
hasMore={item.ctx.hasMore}
|
||||||
showChildReplyLine={item.ctx.showChildReplyLine}
|
showChildReplyLine={item.ctx.showChildReplyLine}
|
||||||
showParentReplyLine={item.ctx.showParentReplyLine}
|
showParentReplyLine={item.ctx.showParentReplyLine}
|
||||||
hasPrecedingItem={!!prev?.ctx.showChildReplyLine}
|
hasPrecedingItem={
|
||||||
|
!!prev?.ctx.showChildReplyLine || hasUnrevealedParents
|
||||||
|
}
|
||||||
onPostReply={onRefresh}
|
onPostReply={onRefresh}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -383,6 +414,8 @@ function PostThreadLoaded({
|
|||||||
onRefresh,
|
onRefresh,
|
||||||
deferParents,
|
deferParents,
|
||||||
treeView,
|
treeView,
|
||||||
|
skeleton.parents.length,
|
||||||
|
maxParents,
|
||||||
_,
|
_,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -393,9 +426,10 @@ function PostThreadLoaded({
|
|||||||
data={posts}
|
data={posts}
|
||||||
keyExtractor={item => item._reactKey}
|
keyExtractor={item => item._reactKey}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
refreshing={isPTRing}
|
|
||||||
onRefresh={onPTR}
|
|
||||||
onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb}
|
onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb}
|
||||||
|
onStartReached={onStartReached}
|
||||||
|
onMomentumScrollEnd={onMomentumScrollEnd}
|
||||||
|
onScrollToTop={onScrollToTop}
|
||||||
maintainVisibleContentPosition={
|
maintainVisibleContentPosition={
|
||||||
isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined
|
isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import {PostCtrls} from '../util/post-ctrls/PostCtrls'
|
|||||||
import {PostHider} from '../util/moderation/PostHider'
|
import {PostHider} from '../util/moderation/PostHider'
|
||||||
import {ContentHider} from '../util/moderation/ContentHider'
|
import {ContentHider} from '../util/moderation/ContentHider'
|
||||||
import {PostAlerts} from '../util/moderation/PostAlerts'
|
import {PostAlerts} from '../util/moderation/PostAlerts'
|
||||||
import {PostSandboxWarning} from '../util/PostSandboxWarning'
|
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {formatCount} from '../util/numeric/format'
|
import {formatCount} from '../util/numeric/format'
|
||||||
@@ -44,6 +43,7 @@ import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
|
|||||||
import {ThreadPost} from '#/state/queries/post-thread'
|
import {ThreadPost} from '#/state/queries/post-thread'
|
||||||
import {useSession} from 'state/session'
|
import {useSession} from 'state/session'
|
||||||
import {WhoCanReply} from '../threadgate/WhoCanReply'
|
import {WhoCanReply} from '../threadgate/WhoCanReply'
|
||||||
|
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||||
|
|
||||||
export function PostThreadItem({
|
export function PostThreadItem({
|
||||||
post,
|
post,
|
||||||
@@ -164,8 +164,6 @@ let PostThreadItemLoaded = ({
|
|||||||
() => countLines(richText?.text) >= MAX_POST_LINES,
|
() => countLines(richText?.text) >= MAX_POST_LINES,
|
||||||
)
|
)
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const hasEngagement = post.likeCount || post.repostCount
|
|
||||||
|
|
||||||
const rootUri = record.reply?.root?.uri || post.uri
|
const rootUri = record.reply?.root?.uri || post.uri
|
||||||
const postHref = React.useMemo(() => {
|
const postHref = React.useMemo(() => {
|
||||||
const urip = new AtUri(post.uri)
|
const urip = new AtUri(post.uri)
|
||||||
@@ -248,7 +246,6 @@ let PostThreadItemLoaded = ({
|
|||||||
testID={`postThreadItem-by-${post.author.handle}`}
|
testID={`postThreadItem-by-${post.author.handle}`}
|
||||||
style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]}
|
style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]}
|
||||||
accessible={false}>
|
accessible={false}>
|
||||||
<PostSandboxWarning />
|
|
||||||
<View style={[styles.layout]}>
|
<View style={[styles.layout]}>
|
||||||
<View style={[styles.layoutAvi, {paddingBottom: 8}]}>
|
<View style={[styles.layoutAvi, {paddingBottom: 8}]}>
|
||||||
<PreviewableUserAvatar
|
<PreviewableUserAvatar
|
||||||
@@ -357,9 +354,16 @@ let PostThreadItemLoaded = ({
|
|||||||
translatorUrl={translatorUrl}
|
translatorUrl={translatorUrl}
|
||||||
needsTranslation={needsTranslation}
|
needsTranslation={needsTranslation}
|
||||||
/>
|
/>
|
||||||
{hasEngagement ? (
|
{post.repostCount !== 0 || post.likeCount !== 0 ? (
|
||||||
|
// Show this section unless we're *sure* it has no engagement.
|
||||||
<View style={[styles.expandedInfo, pal.border]}>
|
<View style={[styles.expandedInfo, pal.border]}>
|
||||||
{post.repostCount ? (
|
{post.repostCount == null && post.likeCount == null && (
|
||||||
|
// If we're still loading and not sure, assume this post has engagement.
|
||||||
|
// This lets us avoid a layout shift for the common case (embedded post with likes/reposts).
|
||||||
|
// TODO: embeds should include metrics to avoid us having to guess.
|
||||||
|
<LoadingPlaceholder width={50} height={20} />
|
||||||
|
)}
|
||||||
|
{post.repostCount != null && post.repostCount !== 0 ? (
|
||||||
<Link
|
<Link
|
||||||
style={styles.expandedInfoItem}
|
style={styles.expandedInfoItem}
|
||||||
href={repostsHref}
|
href={repostsHref}
|
||||||
@@ -374,10 +378,8 @@ let PostThreadItemLoaded = ({
|
|||||||
{pluralize(post.repostCount, 'repost')}
|
{pluralize(post.repostCount, 'repost')}
|
||||||
</Text>
|
</Text>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : null}
|
||||||
<></>
|
{post.likeCount != null && post.likeCount !== 0 ? (
|
||||||
)}
|
|
||||||
{post.likeCount ? (
|
|
||||||
<Link
|
<Link
|
||||||
style={styles.expandedInfoItem}
|
style={styles.expandedInfoItem}
|
||||||
href={likesHref}
|
href={likesHref}
|
||||||
@@ -392,13 +394,9 @@ let PostThreadItemLoaded = ({
|
|||||||
{pluralize(post.likeCount, 'like')}
|
{pluralize(post.likeCount, 'like')}
|
||||||
</Text>
|
</Text>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : null}
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : null}
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
<View style={[s.pl10, s.pr10, s.pb5]}>
|
<View style={[s.pl10, s.pr10, s.pb5]}>
|
||||||
<PostCtrls
|
<PostCtrls
|
||||||
big
|
big
|
||||||
@@ -438,8 +436,6 @@ let PostThreadItemLoaded = ({
|
|||||||
? {marginRight: 4}
|
? {marginRight: 4}
|
||||||
: {marginLeft: 2, marginRight: 2}
|
: {marginLeft: 2, marginRight: 2}
|
||||||
}>
|
}>
|
||||||
<PostSandboxWarning />
|
|
||||||
|
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {PostEmbeds} from '../util/post-embeds'
|
|||||||
import {ContentHider} from '../util/moderation/ContentHider'
|
import {ContentHider} from '../util/moderation/ContentHider'
|
||||||
import {PostAlerts} from '../util/moderation/PostAlerts'
|
import {PostAlerts} from '../util/moderation/PostAlerts'
|
||||||
import {RichText} from '../util/text/RichText'
|
import {RichText} from '../util/text/RichText'
|
||||||
import {PostSandboxWarning} from '../util/PostSandboxWarning'
|
|
||||||
import {PreviewableUserAvatar} from '../util/UserAvatar'
|
import {PreviewableUserAvatar} from '../util/UserAvatar'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
@@ -160,8 +159,6 @@ let FeedItemInner = ({
|
|||||||
href={href}
|
href={href}
|
||||||
noFeedback
|
noFeedback
|
||||||
accessible={false}>
|
accessible={false}>
|
||||||
<PostSandboxWarning />
|
|
||||||
|
|
||||||
<View style={{flexDirection: 'row', gap: 10, paddingLeft: 8}}>
|
<View style={{flexDirection: 'row', gap: 10, paddingLeft: 8}}>
|
||||||
<View style={{width: 52}}>
|
<View style={{width: 52}}>
|
||||||
{isThreadChild && (
|
{isThreadChild && (
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {StyleSheet, View} from 'react-native'
|
|
||||||
import {Text} from './text/Text'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {useSession} from '#/state/session'
|
|
||||||
|
|
||||||
export function PostSandboxWarning() {
|
|
||||||
const {isSandbox} = useSession()
|
|
||||||
const pal = usePalette('default')
|
|
||||||
if (isSandbox) {
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Text
|
|
||||||
type="title-2xl"
|
|
||||||
style={[pal.text, styles.text]}
|
|
||||||
accessible={false}>
|
|
||||||
SANDBOX
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 6,
|
|
||||||
right: 10,
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
fontWeight: 'bold',
|
|
||||||
opacity: 0.07,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -6,6 +6,7 @@ import {H1} from '#/components/Typography'
|
|||||||
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
||||||
import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight'
|
import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight'
|
||||||
import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
|
import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
|
||||||
export function Icons() {
|
export function Icons() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -36,6 +37,14 @@ export function Icons() {
|
|||||||
<CalendarDays size="lg" fill={t.atoms.text.color} />
|
<CalendarDays size="lg" fill={t.atoms.text.color} />
|
||||||
<CalendarDays size="xl" fill={t.atoms.text.color} />
|
<CalendarDays size="xl" fill={t.atoms.text.color} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.flex_row, a.gap_xl]}>
|
||||||
|
<Loader size="xs" fill={t.atoms.text.color} />
|
||||||
|
<Loader size="sm" fill={t.atoms.text.color} />
|
||||||
|
<Loader size="md" fill={t.atoms.text.color} />
|
||||||
|
<Loader size="lg" fill={t.atoms.text.color} />
|
||||||
|
<Loader size="xl" fill={t.atoms.text.color} />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,14 @@ export function Links() {
|
|||||||
style={[a.text_md]}>
|
style={[a.text_md]}>
|
||||||
External
|
External
|
||||||
</InlineLink>
|
</InlineLink>
|
||||||
<InlineLink to="https://bsky.social" style={[a.text_md]}>
|
<InlineLink to="https://bsky.social" style={[a.text_md, t.atoms.text]}>
|
||||||
<H3>External with custom children</H3>
|
<H3>External with custom children</H3>
|
||||||
</InlineLink>
|
</InlineLink>
|
||||||
|
<InlineLink
|
||||||
|
to="https://bsky.social"
|
||||||
|
style={[a.text_md, t.atoms.text_contrast_low]}>
|
||||||
|
External with custom children
|
||||||
|
</InlineLink>
|
||||||
<InlineLink
|
<InlineLink
|
||||||
to="https://bsky.social"
|
to="https://bsky.social"
|
||||||
warnOnMismatchingTextChild
|
warnOnMismatchingTextChild
|
||||||
|
|||||||
@@ -9,14 +9,13 @@ import {FEEDBACK_FORM_URL, HELP_DESK_URL} from 'lib/constants'
|
|||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {Trans, msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
|
|
||||||
export function DesktopRightNav({routeName}: {routeName: string}) {
|
export function DesktopRightNav({routeName}: {routeName: string}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const palError = usePalette('error')
|
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {isSandbox, hasSession, currentAccount} = useSession()
|
const {hasSession, currentAccount} = useSession()
|
||||||
|
|
||||||
const {isTablet} = useWebMediaQueries()
|
const {isTablet} = useWebMediaQueries()
|
||||||
if (isTablet) {
|
if (isTablet) {
|
||||||
@@ -49,13 +48,6 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
|
|||||||
paddingTop: hasSession ? 0 : 18,
|
paddingTop: hasSession ? 0 : 18,
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
{isSandbox ? (
|
|
||||||
<View style={[palError.view, styles.messageLine, s.p10]}>
|
|
||||||
<Text type="md" style={[palError.text, s.bold]}>
|
|
||||||
<Trans>SANDBOX. Posts and accounts are not permanent.</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
<View style={[{flexWrap: 'wrap'}, s.flexRow]}>
|
<View style={[{flexWrap: 'wrap'}, s.flexRow]}>
|
||||||
{hasSession && (
|
{hasSession && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in New Issue
Block a user