From ae9f893723819856ff2f422a139ea199308c9b38 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 16 Feb 2024 11:50:24 -0600 Subject: [PATCH 01/13] Some button updates (#2889) * Some button updates * Better name --- src/components/Button.tsx | 102 +++++++++++++++++++++++++------------- 1 file changed, 67 insertions(+), 35 deletions(-) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 68cee437..e401bda2 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -27,7 +27,7 @@ export type ButtonColor = | 'gradient_sunset' | 'gradient_nordic' | 'gradient_bonfire' -export type ButtonSize = 'small' | 'large' +export type ButtonSize = 'tiny' | 'small' | 'large' export type ButtonShape = 'round' | 'square' | 'default' export type VariantProps = { /** @@ -48,25 +48,32 @@ export type VariantProps = { shape?: ButtonShape } -export type ButtonProps = React.PropsWithChildren< - Pick & - AccessibilityProps & - VariantProps & { - testID?: string - label: string - style?: StyleProp - } -> +export type ButtonState = { + hovered: boolean + focused: boolean + pressed: boolean + disabled: boolean +} + +export type ButtonContext = VariantProps & ButtonState + +export type ButtonProps = Pick< + PressableProps, + 'disabled' | 'onPress' | 'testID' +> & + AccessibilityProps & + VariantProps & { + testID?: string + label: string + style?: StyleProp + children: + | React.ReactNode + | string + | ((context: ButtonContext) => React.ReactNode | string) + } export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean} -const Context = React.createContext< - VariantProps & { - hovered: boolean - focused: boolean - pressed: boolean - disabled: boolean - } ->({ +const Context = React.createContext({ hovered: false, focused: false, pressed: false, @@ -277,6 +284,8 @@ export function Button({ baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md) } else if (size === 'small') { 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') { if (size === 'large') { @@ -287,12 +296,18 @@ export function Button({ } } else if (size === 'small') { baseStyles.push({height: 40, width: 40}) + } else if (size === 'tiny') { + baseStyles.push({height: 20, width: 20}) } if (shape === 'round') { baseStyles.push(a.rounded_full) } 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]) - const context = React.useMemo( + const context = React.useMemo( () => ({ ...state, variant, @@ -349,6 +364,8 @@ export function Button({ [state, variant, color, size, disabled], ) + const flattenedBaseStyles = flatten(baseStyles) + return ( {variant === 'gradient' && ( - + + + )} {typeof children === 'string' ? ( {children} + ) : typeof children === 'function' ? ( + children(context) ) : ( children )} @@ -493,6 +519,8 @@ export function useSharedButtonTextStyles() { if (size === 'large') { baseStyles.push(a.text_md, android({paddingBottom: 1})) + } else if (size === 'tiny') { + baseStyles.push(a.text_xs, android({paddingBottom: 1})) } else { baseStyles.push(a.text_sm, android({paddingBottom: 1})) } @@ -514,9 +542,11 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) { export function ButtonIcon({ icon: Comp, position, + size: iconSize, }: { icon: React.ComponentType position?: 'left' | 'right' + size?: SVGIconProps['size'] }) { const {size, disabled} = useButtonContext() const textStyles = useSharedButtonTextStyles() @@ -532,7 +562,9 @@ export function ButtonIcon({ }, ]}> From e303940eaa4ba5742a6b8c579e5f814345786d69 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 16 Feb 2024 11:54:40 -0600 Subject: [PATCH 02/13] Bump contrast on dim mode for old ds (#2888) --- src/lib/themes.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/lib/themes.ts b/src/lib/themes.ts index 9a3880b9..f75ac8ab 100644 --- a/src/lib/themes.ts +++ b/src/lib/themes.ts @@ -344,6 +344,25 @@ export const dimTheme: Theme = { default: { ...darkTheme.palette.default, 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, }, }, } From c5641ac2b7bdcfdc4627175c7125131faf7c9744 Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 16 Feb 2024 18:07:47 +0000 Subject: [PATCH 03/13] Fix jumps when navigating into long threads (#2878) * Reveal parents in chunks to fix scroll jumps Co-authored-by: Hailey * Prevent layout jump when navigating to QT due to missing metrics --------- Co-authored-by: Hailey --- src/view/com/post-thread/PostThread.tsx | 86 ++++++++++++++------- src/view/com/post-thread/PostThreadItem.tsx | 28 +++---- 2 files changed, 74 insertions(+), 40 deletions(-) diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 2b08bc40..434f018f 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -43,10 +43,13 @@ import { usePreferencesQuery, } from '#/state/queries/preferences' import {useSession} from '#/state/session' -import {isAndroid, isNative} from '#/platform/detection' -import {logger} from '#/logger' +import {isAndroid, isNative, isWeb} from '#/platform/detection' 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 = { // 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. @@ -165,8 +168,10 @@ function PostThreadLoaded({ const {isMobile, isTabletOrMobile} = useWebMediaQueries() const ref = useRef(null) const highlightedPostRef = useRef(null) - const [maxVisible, setMaxVisible] = React.useState(100) - const [isPTRing, setIsPTRing] = React.useState(false) + const [maxParents, setMaxParents] = React.useState( + isWeb ? Infinity : PARENTS_CHUNK_SIZE, + ) + const [maxReplies, setMaxReplies] = React.useState(100) const treeView = React.useMemo( () => !!threadViewPrefs.lab_treeViewEnabled && hasBranchingReplies(thread), [threadViewPrefs, thread], @@ -206,10 +211,18 @@ function PostThreadLoaded({ // maintainVisibleContentPosition and onContentSizeChange // to "hold onto" the correct row instead of the first one. } else { - // Everything is loaded. - arr.push(TOP_COMPONENT) - for (const parent of parents) { - arr.push(parent) + // Everything is loaded + let startIndex = Math.max(0, parents.length - maxParents) + if (startIndex === 0) { + 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) { arr.push(CHILD_SPINNER) } else { - for (const reply of replies) { - arr.push(reply) + for (let i = 0; i < replies.length; i++) { + arr.push(replies[i]) + if (i === maxReplies) { + arr.push(LOAD_MORE) + break + } } arr.push(BOTTOM_COMPONENT) } } - if (arr.length > maxVisible) { - arr = arr.slice(0, maxVisible).concat([LOAD_MORE]) - } 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. // On native, we rely on `maintainVisibleContentPosition` instead. @@ -258,15 +272,28 @@ function PostThreadLoaded({ } }, [thread]) - const onPTR = React.useCallback(async () => { - setIsPTRing(true) - try { - await onRefresh() - } catch (err) { - logger.error('Failed to refresh posts thread', {message: err}) + // On native, we reveal parents in chunks. Although they're all already + // loaded and FlatList already has its own virtualization, unfortunately FlatList + // has a bug that causes the content to jump around if too many items are getting + // prepended at once. It also jumps around if items get prepended during scroll. + // To work around this, we prepend rows after scroll bumps against the top and rests. + const needsBumpMaxParents = React.useRef(false) + const onStartReached = React.useCallback(() => { + if (maxParents < skeleton.parents.length) { + needsBumpMaxParents.current = true } - setIsPTRing(false) - }, [setIsPTRing, onRefresh]) + }, [maxParents, skeleton.parents.length]) + 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( ({item, index}: {item: RowItem; index: number}) => { @@ -301,7 +328,7 @@ function PostThreadLoaded({ } else if (item === LOAD_MORE) { return ( setMaxVisible(n => n + 50)} + onPress={() => setMaxReplies(n => n + 50)} style={[pal.border, pal.view, styles.itemContainer]} accessibilityLabel={_(msg`Load more posts`)} accessibilityHint=""> @@ -345,6 +372,8 @@ function PostThreadLoaded({ const next = isThreadPost(posts[index - 1]) ? (posts[index - 1] as ThreadPost) : undefined + const hasUnrevealedParents = + index === 0 && maxParents < skeleton.parents.length return ( @@ -383,6 +414,8 @@ function PostThreadLoaded({ onRefresh, deferParents, treeView, + skeleton.parents.length, + maxParents, _, ], ) @@ -393,9 +426,10 @@ function PostThreadLoaded({ data={posts} keyExtractor={item => item._reactKey} renderItem={renderItem} - refreshing={isPTRing} - onRefresh={onPTR} onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb} + onStartReached={onStartReached} + onMomentumScrollEnd={onMomentumScrollEnd} + onScrollToTop={onScrollToTop} maintainVisibleContentPosition={ isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined } diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 826d0d16..ced6d0d6 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -44,6 +44,7 @@ import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' import {ThreadPost} from '#/state/queries/post-thread' import {useSession} from 'state/session' import {WhoCanReply} from '../threadgate/WhoCanReply' +import {LoadingPlaceholder} from '../util/LoadingPlaceholder' export function PostThreadItem({ post, @@ -164,8 +165,6 @@ let PostThreadItemLoaded = ({ () => countLines(richText?.text) >= MAX_POST_LINES, ) const {currentAccount} = useSession() - const hasEngagement = post.likeCount || post.repostCount - const rootUri = record.reply?.root?.uri || post.uri const postHref = React.useMemo(() => { const urip = new AtUri(post.uri) @@ -357,9 +356,16 @@ let PostThreadItemLoaded = ({ translatorUrl={translatorUrl} needsTranslation={needsTranslation} /> - {hasEngagement ? ( + {post.repostCount !== 0 || post.likeCount !== 0 ? ( + // Show this section unless we're *sure* it has no engagement. - {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. + + )} + {post.repostCount != null && post.repostCount !== 0 ? ( - ) : ( - <> - )} - {post.likeCount ? ( + ) : null} + {post.likeCount != null && post.likeCount !== 0 ? ( - ) : ( - <> - )} + ) : null} - ) : ( - <> - )} + ) : null} Date: Fri, 16 Feb 2024 12:07:57 -0600 Subject: [PATCH 04/13] Darken splash (#2892) * Darken splash * We must go darker --- src/Splash.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Splash.tsx b/src/Splash.tsx index 80d0a66e..d8405005 100644 --- a/src/Splash.tsx +++ b/src/Splash.tsx @@ -263,7 +263,12 @@ export function Splash(props: React.PropsWithChildren) { )} From 1d729721e553848037700688e2f1ccde333a8c84 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 16 Feb 2024 13:25:07 -0600 Subject: [PATCH 05/13] Link updates (#2890) * Link updates, add atoms * Update comments * Support download * Don't open new window for download --- src/alf/atoms.ts | 36 ++++++++++++ src/components/Link.tsx | 85 +++++++++++++++++----------- src/view/screens/Storybook/Links.tsx | 7 ++- 3 files changed, 95 insertions(+), 33 deletions(-) diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index bbf7e324..f75e8ffe 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -122,6 +122,9 @@ export const atoms = { flex_shrink: { flexShrink: 1, }, + justify_start: { + justifyContent: 'flex-start', + }, justify_center: { justifyContent: 'center', }, @@ -140,10 +143,31 @@ export const atoms = { align_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_left: { + textAlign: 'left', + }, text_center: { textAlign: 'center', }, @@ -195,10 +219,16 @@ export const atoms = { font_bold: { fontWeight: tokens.fontWeight.semibold, }, + italic: { + fontStyle: 'italic', + }, /* * Border */ + border_0: { + borderWidth: 0, + }, border: { borderWidth: 1, }, @@ -208,6 +238,12 @@ export const atoms = { border_b: { borderBottomWidth: 1, }, + border_l: { + borderLeftWidth: 1, + }, + border_r: { + borderRightWidth: 1, + }, /* * Shadow diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 763f07ca..afd30b5e 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -13,7 +13,7 @@ import {sanitizeUrl} from '@braintree/sanitize-url' import {useInteractionState} from '#/components/hooks/useInteractionState' 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 {AllNavigatorParams, NavigationProp} from '#/lib/routes/types' import { @@ -35,6 +35,13 @@ type BaseLinkProps = Pick< Parameters>[0], 'to' > & { + testID?: string + + /** + * Label for a11y. Defaults to the href. + */ + label?: string + /** * 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. */ 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({ @@ -53,6 +72,7 @@ export function useLink({ displayText, action = 'push', warnOnMismatchingTextChild, + onPress: outerOnPress, }: BaseLinkProps & { displayText: string }) { @@ -66,6 +86,8 @@ export function useLink({ const onPress = React.useCallback( (e: GestureResponderEvent) => { + outerOnPress?.(e) + const requiresWarning = Boolean( warnOnMismatchingTextChild && displayText && @@ -132,6 +154,7 @@ export function useLink({ displayText, closeModal, openModal, + outerOnPress, ], ) @@ -143,16 +166,7 @@ export function useLink({ } export type LinkProps = Omit & - Omit & { - /** - * Label for a11y. Defaults to the href. - */ - label?: string - /** - * Web-only attribute. Sets `download` attr on web. - */ - download?: string - } + Omit /** * A interactive element that renders as a `` tag on the web. On mobile it @@ -166,6 +180,7 @@ export function Link({ children, to, action = 'push', + onPress: outerOnPress, download, ...rest }: LinkProps) { @@ -173,24 +188,26 @@ export function Link({ to, displayText: typeof children === 'string' ? children : '', action, + onPress: outerOnPress, }) return (