diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/components/Link.tsx | 12 | ||||
-rw-r--r-- | src/view/com/notifications/NotificationFeedItem.tsx | 392 |
2 files changed, 196 insertions, 208 deletions
diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 8bebecbc8..b5f0bc958 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -1,12 +1,12 @@ import React from 'react' -import {GestureResponderEvent} from 'react-native' +import {type GestureResponderEvent} from 'react-native' import {sanitizeUrl} from '@braintree/sanitize-url' import {StackActions, useLinkProps} from '@react-navigation/native' import {BSKY_DOWNLOAD_URL} from '#/lib/constants' import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped' import {useOpenLink} from '#/lib/hooks/useOpenLink' -import {AllNavigatorParams} from '#/lib/routes/types' +import {type AllNavigatorParams} from '#/lib/routes/types' import {shareUrl} from '#/lib/sharing' import { convertBskyAppUrlIfNeeded, @@ -16,10 +16,10 @@ import { } from '#/lib/strings/url-helpers' import {isNative, isWeb} from '#/platform/detection' import {useModalControls} from '#/state/modals' -import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf' -import {Button, ButtonProps} from '#/components/Button' +import {atoms as a, flatten, type TextStyleProp, useTheme, web} from '#/alf' +import {Button, type ButtonProps} from '#/components/Button' import {useInteractionState} from '#/components/hooks/useInteractionState' -import {Text, TextProps} from '#/components/Typography' +import {Text, type TextProps} from '#/components/Typography' import {router} from '#/routes' /** @@ -267,7 +267,7 @@ export function Link({ export type InlineLinkProps = React.PropsWithChildren< BaseLinkProps & TextStyleProp & - Pick<TextProps, 'selectable' | 'numberOfLines'> & + Pick<TextProps, 'selectable' | 'numberOfLines' | 'emoji'> & Pick<ButtonProps, 'label' | 'accessibilityHint'> & { disableUnderline?: boolean title?: TextProps['title'] diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx index 84694fe3b..8875ec02e 100644 --- a/src/view/com/notifications/NotificationFeedItem.tsx +++ b/src/view/com/notifications/NotificationFeedItem.tsx @@ -1,25 +1,27 @@ -import React, { +import { memo, type ReactElement, + useCallback, useEffect, useMemo, useState, } from 'react' import { Animated, + type GestureResponderEvent, Pressable, StyleSheet, TouchableOpacity, View, } from 'react-native' import { - AppBskyActorDefs, - AppBskyFeedDefs, + type AppBskyActorDefs, + type AppBskyFeedDefs, AppBskyFeedPost, AppBskyGraphFollow, moderateProfile, - ModerationDecision, - ModerationOpts, + type ModerationDecision, + type ModerationOpts, } from '@atproto/api' import {AtUri} from '@atproto/api' import {TID} from '@atproto/common-web' @@ -31,18 +33,22 @@ import {useQueryClient} from '@tanstack/react-query' import {useAnimatedValue} from '#/lib/hooks/useAnimatedValue' import {usePalette} from '#/lib/hooks/usePalette' import {makeProfileLink} from '#/lib/routes/links' -import {NavigationProp} from '#/lib/routes/types' +import {type NavigationProp} from '#/lib/routes/types' import {forceLTR} from '#/lib/strings/bidi' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {niceDate} from '#/lib/strings/time' -import {colors, s} from '#/lib/styles' +import {s} from '#/lib/styles' import {logger} from '#/logger' -import {isWeb} from '#/platform/detection' import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const' -import {FeedNotification} from '#/state/queries/notifications/feed' -import {precacheProfile} from '#/state/queries/profile' +import {type FeedNotification} from '#/state/queries/notifications/feed' +import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache' import {useAgent} from '#/state/session' +import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard' +import {Post} from '#/view/com/post/Post' +import {formatCount} from '#/view/com/util/numeric/format' +import {TimeElapsed} from '#/view/com/util/TimeElapsed' +import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import { @@ -53,19 +59,13 @@ import {Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled} from '#/compon import {PersonPlus_Filled_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person' import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/Repost' import {StarterPack} from '#/components/icons/StarterPack' -import {Link as NewLink} from '#/components/Link' +import {InlineLinkText, Link} from '#/components/Link' import * as MediaPreview from '#/components/MediaPreview' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard' import {SubtleWebHover} from '#/components/SubtleWebHover' +import {Text} from '#/components/Typography' import * as bsky from '#/types/bsky' -import {FeedSourceCard} from '../feeds/FeedSourceCard' -import {Post} from '../post/Post' -import {Link, TextLink} from '../util/Link' -import {formatCount} from '../util/numeric/format' -import {Text} from '../util/text/Text' -import {TimeElapsed} from '../util/TimeElapsed' -import {PreviewableUserAvatar, UserAvatar} from '../util/UserAvatar' const MAX_AUTHORS = 5 @@ -90,8 +90,8 @@ let NotificationFeedItem = ({ }): React.ReactNode => { const queryClient = useQueryClient() const pal = usePalette('default') - const {_, i18n} = useLingui() const t = useTheme() + const {_, i18n} = useLingui() const [isAuthorsExpanded, setAuthorsExpanded] = useState<boolean>(false) const itemHref = useMemo(() => { if (item.type === 'post-like' || item.type === 'repost') { @@ -116,12 +116,16 @@ let NotificationFeedItem = ({ return '' }, [item]) - const onToggleAuthorsExpanded = () => { + const onToggleAuthorsExpanded = (e?: GestureResponderEvent) => { + if (e) { + e.preventDefault() + e.stopPropagation() + } setAuthorsExpanded(currentlyExpanded => !currentlyExpanded) } - const onBeforePress = React.useCallback(() => { - precacheProfile(queryClient, item.notification.author) + const onBeforePress = useCallback(() => { + unstableCacheProfileView(queryClient, item.notification.author) }, [queryClient, item.notification.author]) const authors: Author[] = useMemo(() => { @@ -139,7 +143,11 @@ let NotificationFeedItem = ({ ] }, [item, moderationOpts]) - const [hover, setHover] = React.useState(false) + const niceTimestamp = niceDate(i18n, item.notification.indexedAt) + const firstAuthor = authors[0] + const firstAuthorName = sanitizeDisplayName( + firstAuthor.profile.displayName || firstAuthor.profile.handle, + ) if (item.subjectUri && !item.subject && item.type !== 'feedgen-like') { // don't render anything if the target post was deleted or unfindable @@ -156,42 +164,29 @@ let NotificationFeedItem = ({ } const isHighlighted = highlightUnread && !item.notification.isRead return ( - <Link - testID={`feedItem-by-${item.notification.author.handle}`} - href={itemHref} - noFeedback - accessible={false}> - <Post - post={item.subject} - style={ - isHighlighted && { - backgroundColor: pal.colors.unreadNotifBg, - borderColor: pal.colors.unreadNotifBorder, - } + <Post + post={item.subject} + style={ + isHighlighted && { + backgroundColor: pal.colors.unreadNotifBg, + borderColor: pal.colors.unreadNotifBorder, } - hideTopBorder={hideTopBorder} - /> - </Link> + } + hideTopBorder={hideTopBorder} + /> ) } - const niceTimestamp = niceDate(i18n, item.notification.indexedAt) - const firstAuthor = authors[0] - const firstAuthorName = sanitizeDisplayName( - firstAuthor.profile.displayName || firstAuthor.profile.handle, - ) const firstAuthorLink = ( - <TextLink + <InlineLinkText key={firstAuthor.href} - style={[pal.text, s.bold]} - href={firstAuthor.href} - text={ - <Text emoji style={[pal.text, s.bold]}> - {forceLTR(firstAuthorName)} - </Text> - } + style={[t.atoms.text, a.font_bold, a.text_md, a.leading_tight]} + to={firstAuthor.href} disableMismatchWarning - /> + emoji + label={_(msg`Go to ${firstAuthorName}'s profile`)}> + {forceLTR(firstAuthorName)} + </InlineLinkText> ) const additionalAuthorsCount = authors.length - 1 const hasMultipleAuthors = additionalAuthorsCount > 0 @@ -223,7 +218,7 @@ let NotificationFeedItem = ({ notificationContent = hasMultipleAuthors ? ( <Trans> {firstAuthorLink} and{' '} - <Text style={[pal.text, s.bold]}> + <Text style={[a.text_md, a.font_bold, a.leading_snug]}> <Plural value={additionalAuthorsCount} one={`${formattedAuthorsCount} other`} @@ -247,7 +242,7 @@ let NotificationFeedItem = ({ notificationContent = hasMultipleAuthors ? ( <Trans> {firstAuthorLink} and{' '} - <Text style={[pal.text, s.bold]}> + <Text style={[a.text_md, a.font_bold, a.leading_snug]}> <Plural value={additionalAuthorsCount} one={`${formattedAuthorsCount} other`} @@ -304,7 +299,7 @@ let NotificationFeedItem = ({ notificationContent = hasMultipleAuthors ? ( <Trans> {firstAuthorLink} and{' '} - <Text style={[pal.text, s.bold]}> + <Text style={[a.text_md, a.font_bold, a.leading_snug]}> <Plural value={additionalAuthorsCount} one={`${formattedAuthorsCount} other`} @@ -330,7 +325,7 @@ let NotificationFeedItem = ({ notificationContent = hasMultipleAuthors ? ( <Trans> {firstAuthorLink} and{' '} - <Text style={[pal.text, s.bold]}> + <Text style={[a.text_md, a.font_bold, a.leading_snug]}> <Plural value={additionalAuthorsCount} one={`${formattedAuthorsCount} other`} @@ -354,7 +349,7 @@ let NotificationFeedItem = ({ notificationContent = hasMultipleAuthors ? ( <Trans> {firstAuthorLink} and{' '} - <Text style={[pal.text, s.bold]}> + <Text style={[a.text_md, a.font_bold, a.leading_snug]}> <Plural value={additionalAuthorsCount} one={`${formattedAuthorsCount} other`} @@ -378,23 +373,24 @@ let NotificationFeedItem = ({ return ( <Link + label={a11yLabel} testID={`feedItem-by-${item.notification.author.handle}`} style={[ - styles.outer, - pal.border, + a.flex_row, + a.align_start, + {padding: 10}, + a.pr_lg, + t.atoms.border_contrast_low, item.notification.isRead ? undefined : { backgroundColor: pal.colors.unreadNotifBg, borderColor: pal.colors.unreadNotifBorder, }, - {borderTopWidth: hideTopBorder ? 0 : StyleSheet.hairlineWidth}, + !hideTopBorder && a.border_t, a.overflow_hidden, ]} - href={itemHref} - noFeedback - accessibilityHint="" - accessibilityLabel={a11yLabel} + to={itemHref} accessible={!isAuthorsExpanded} accessibilityActions={ hasMultipleAuthors @@ -424,78 +420,92 @@ let NotificationFeedItem = ({ if (e.nativeEvent.actionName === 'toggleAuthorsExpanded') { onToggleAuthorsExpanded() } - }} - onPointerEnter={() => { - setHover(true) - }} - onPointerLeave={() => { - setHover(false) }}> - <SubtleWebHover hover={hover} /> - <View style={[styles.layoutIcon, a.pr_sm]}> - {/* TODO: Prevent conditional rendering and move toward composable - notifications for clearer accessibility labeling */} - {icon} - </View> - <View style={styles.layoutContent}> - <ExpandListPressable - hasMultipleAuthors={hasMultipleAuthors} - onToggleAuthorsExpanded={onToggleAuthorsExpanded}> - <CondensedAuthorsList - visible={!isAuthorsExpanded} - authors={authors} - onToggleAuthorsExpanded={onToggleAuthorsExpanded} - showDmButton={item.type === 'starterpack-joined'} - /> - <ExpandedAuthorsList visible={isAuthorsExpanded} authors={authors} /> - <Text - style={[styles.meta, a.self_start, pal.text]} - accessibilityHint="" - accessibilityLabel={a11yLabel}> - {notificationContent} - <TimeElapsed timestamp={item.notification.indexedAt}> - {({timeElapsed}) => ( - <> - {/* make sure there's whitespace around the middot -sfn */} - <Text style={[pal.textLight]}> · </Text> - <Text style={[pal.textLight]} title={niceTimestamp}> - {timeElapsed} - </Text> - </> - )} - </TimeElapsed> - </Text> - </ExpandListPressable> - {item.type === 'post-like' || item.type === 'repost' ? ( - <AdditionalPostText post={item.subject} /> - ) : null} - {item.type === 'feedgen-like' && item.subjectUri ? ( - <FeedSourceCard - feedUri={item.subjectUri} - style={[ - t.atoms.bg, - t.atoms.border_contrast_low, - a.border, - styles.feedcard, - ]} - showLikes - /> - ) : null} - {item.type === 'starterpack-joined' ? ( - <View> - <View - style={[ - a.border, - a.p_sm, - a.rounded_sm, - a.mt_sm, - t.atoms.border_contrast_low, - ]}> - <StarterPackCard starterPack={item.subject} /> - </View> + {({hovered}) => ( + <> + <SubtleWebHover hover={hovered} /> + <View style={[styles.layoutIcon, a.pr_sm]}> + {/* TODO: Prevent conditional rendering and move toward composable + notifications for clearer accessibility labeling */} + {icon} </View> - ) : null} - </View> + <View style={[a.flex_1]}> + <ExpandListPressable + hasMultipleAuthors={hasMultipleAuthors} + onToggleAuthorsExpanded={onToggleAuthorsExpanded}> + <CondensedAuthorsList + visible={!isAuthorsExpanded} + authors={authors} + onToggleAuthorsExpanded={onToggleAuthorsExpanded} + showDmButton={item.type === 'starterpack-joined'} + /> + <ExpandedAuthorsList + visible={isAuthorsExpanded} + authors={authors} + /> + <Text + style={[ + a.flex_row, + a.flex_wrap, + a.pb_2xs, + {paddingTop: 6}, + a.self_start, + a.text_md, + a.leading_snug, + ]} + accessibilityHint="" + accessibilityLabel={a11yLabel}> + {notificationContent} + <TimeElapsed timestamp={item.notification.indexedAt}> + {({timeElapsed}) => ( + <> + {/* make sure there's whitespace around the middot -sfn */} + <Text style={[a.text_md, t.atoms.text_contrast_medium]}> + {' '} + ·{' '} + </Text> + <Text + style={[a.text_md, t.atoms.text_contrast_medium]} + title={niceTimestamp}> + {timeElapsed} + </Text> + </> + )} + </TimeElapsed> + </Text> + </ExpandListPressable> + {item.type === 'post-like' || item.type === 'repost' ? ( + <AdditionalPostText post={item.subject} /> + ) : null} + {item.type === 'feedgen-like' && item.subjectUri ? ( + <FeedSourceCard + feedUri={item.subjectUri} + style={[ + t.atoms.bg, + t.atoms.border_contrast_low, + a.border, + styles.feedcard, + ]} + showLikes + /> + ) : null} + {item.type === 'starterpack-joined' ? ( + <View> + <View + style={[ + a.border, + a.p_sm, + a.rounded_sm, + a.mt_sm, + t.atoms.border_contrast_low, + ]}> + <StarterPackCard starterPack={item.subject} /> + </View> + </View> + ) : null} + </View> + </> + )} </Link> ) } @@ -509,7 +519,7 @@ function ExpandListPressable({ }: { hasMultipleAuthors: boolean children: React.ReactNode - onToggleAuthorsExpanded: () => void + onToggleAuthorsExpanded: (e: GestureResponderEvent) => void }) { if (hasMultipleAuthors) { return ( @@ -529,7 +539,7 @@ function SayHelloBtn({profile}: {profile: AppBskyActorDefs.ProfileView}) { const {_} = useLingui() const agent = useAgent() const navigation = useNavigation<NavigationProp>() - const [isLoading, setIsLoading] = React.useState(false) + const [isLoading, setIsLoading] = useState(false) if ( profile.associated?.chat?.allowIncoming === 'none' || @@ -580,15 +590,15 @@ function CondensedAuthorsList({ }: { visible: boolean authors: Author[] - onToggleAuthorsExpanded: () => void + onToggleAuthorsExpanded: (e: GestureResponderEvent) => void showDmButton?: boolean }) { - const pal = usePalette('default') + const t = useTheme() const {_} = useLingui() if (!visible) { return ( - <View style={styles.avis}> + <View style={[a.flex_row, a.align_center]}> <TouchableOpacity style={styles.expandedAuthorsCloseBtn} onPress={onToggleAuthorsExpanded} @@ -599,9 +609,9 @@ function CondensedAuthorsList({ )}> <ChevronUpIcon size="md" - style={[styles.expandedAuthorsCloseBtnIcon, pal.text]} + style={[a.ml_xs, a.mr_md, t.atoms.text_contrast_high]} /> - <Text type="sm-medium" style={pal.text}> + <Text style={[a.text_md, t.atoms.text_contrast_high]}> <Trans context="action">Hide</Trans> </Text> </TouchableOpacity> @@ -610,7 +620,7 @@ function CondensedAuthorsList({ } if (authors.length === 1) { return ( - <View style={[styles.avis]}> + <View style={[a.flex_row, a.align_center]}> <PreviewableUserAvatar size={35} profile={authors[0].profile} @@ -625,7 +635,7 @@ function CondensedAuthorsList({ <TouchableOpacity accessibilityRole="none" onPress={onToggleAuthorsExpanded}> - <View style={styles.avis}> + <View style={[a.flex_row, a.align_center]}> {authors.slice(0, MAX_AUTHORS).map(author => ( <View key={author.href} style={s.mr5}> <PreviewableUserAvatar @@ -637,13 +647,18 @@ function CondensedAuthorsList({ </View> ))} {authors.length > MAX_AUTHORS ? ( - <Text style={[styles.aviExtraCount, pal.textLight]}> + <Text + style={[ + a.font_bold, + {paddingLeft: 6}, + t.atoms.text_contrast_medium, + ]}> +{authors.length - MAX_AUTHORS} </Text> ) : undefined} <ChevronDownIcon size="md" - style={[styles.expandedAuthorsCloseBtnIcon, pal.textLight]} + style={[a.mx_xs, t.atoms.text_contrast_medium]} /> </View> </TouchableOpacity> @@ -658,7 +673,7 @@ function ExpandedAuthorsList({ authors: Author[] }) { const {_} = useLingui() - const pal = usePalette('default') + const t = useTheme() const heightInterp = useAnimatedValue(visible ? 1 : 0) const targetHeight = authors.length * (EXPANDED_AUTHOR_EL_HEIGHT + 10) /*10=margin*/ @@ -677,7 +692,7 @@ function ExpandedAuthorsList({ <Animated.View style={[a.overflow_hidden, heightStyle]}> {visible && authors.map(author => ( - <NewLink + <Link key={author.profile.did} label={author.profile.displayName || author.profile.handle} accessibilityHint={_(msg`Opens this profile`)} @@ -686,7 +701,7 @@ function ExpandedAuthorsList({ handle: author.profile.handle, })} style={styles.expandedAuthor}> - <View style={styles.expandedAuthorAvi}> + <View style={[a.mr_sm]}> <ProfileHoverCard did={author.profile.did}> <UserAvatar size={35} @@ -696,30 +711,42 @@ function ExpandedAuthorsList({ /> </ProfileHoverCard> </View> - <View style={s.flex1}> - <Text - type="lg-bold" - numberOfLines={1} - style={pal.text} - lineHeight={1.2}> - <Text emoji type="lg-bold" style={pal.text} lineHeight={1.2}> + <View style={[a.flex_1]}> + <View style={[a.flex_row, a.align_end]}> + <Text + numberOfLines={1} + emoji + style={[ + a.text_md, + a.font_bold, + a.leading_tight, + {maxWidth: '70%'}, + ]}> {sanitizeDisplayName( author.profile.displayName || author.profile.handle, )} - </Text>{' '} - <Text style={[pal.textLight]} lineHeight={1.2}> + </Text> + <Text + numberOfLines={1} + style={[ + a.pl_xs, + a.text_md, + a.leading_tight, + a.flex_shrink, + t.atoms.text_contrast_medium, + ]}> {sanitizeHandle(author.profile.handle, '@')} </Text> - </Text> + </View> </View> - </NewLink> + </Link> ))} </Animated.View> ) } function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) { - const pal = usePalette('default') + const t = useTheme() if ( post && bsky.dangerousIsType<AppBskyFeedPost.Record>( @@ -732,7 +759,9 @@ function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) { return ( <> {text?.length > 0 && ( - <Text emoji style={pal.textLight}> + <Text + emoji + style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}> {text} </Text> )} @@ -746,18 +775,6 @@ function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) { } const styles = StyleSheet.create({ - pointer: isWeb - ? { - // @ts-ignore web only - cursor: 'pointer', - } - : {}, - - outer: { - padding: 10, - paddingRight: 15, - flexDirection: 'row', - }, layoutIcon: { width: 60, alignItems: 'flex-end', @@ -767,27 +784,6 @@ const styles = StyleSheet.create({ marginRight: 10, marginTop: 4, }, - layoutContent: { - flex: 1, - }, - avis: { - flexDirection: 'row', - alignItems: 'center', - }, - aviExtraCount: { - fontWeight: '600', - paddingLeft: 6, - }, - meta: { - flexDirection: 'row', - flexWrap: 'wrap', - paddingTop: 6, - paddingBottom: 2, - }, - postText: { - paddingBottom: 5, - color: colors.black, - }, additionalPostImages: { marginTop: 5, marginLeft: 2, @@ -798,7 +794,6 @@ const styles = StyleSheet.create({ paddingVertical: 12, marginTop: 6, }, - addedContainer: { paddingTop: 4, paddingLeft: 36, @@ -812,17 +807,10 @@ const styles = StyleSheet.create({ paddingTop: 10, paddingBottom: 6, }, - expandedAuthorsCloseBtnIcon: { - marginLeft: 4, - marginRight: 4, - }, expandedAuthor: { flexDirection: 'row', alignItems: 'center', marginTop: 10, height: EXPANDED_AUTHOR_EL_HEIGHT, }, - expandedAuthorAvi: { - marginRight: 5, - }, }) |