diff --git a/src/view/com/notifications/NotificationFeed.tsx b/src/view/com/notifications/NotificationFeed.tsx index 82b500807..f81d74444 100644 --- a/src/view/com/notifications/NotificationFeed.tsx +++ b/src/view/com/notifications/NotificationFeed.tsx @@ -16,7 +16,7 @@ import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {List, type ListProps, type ListRef} from '#/view/com/util/List' import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' -import {atoms as a, platform} from '#/alf' +import {atoms as a, platform, useTheme} from '#/alf' import {Bell_Stroke2_Corner0_Rounded as BellIcon} from '#/components/icons/Bell' import {NotificationFeedItem} from './NotificationFeedItem' @@ -50,6 +50,7 @@ export function NotificationFeed({ ListHeaderComponent?: ListProps['ListHeaderComponent'] refreshNotifications: () => Promise }) { + const t = useTheme() const initialNumToRender = useInitialNumToRender() const [isPTRing, setIsPTRing] = useState(false) const {t: l} = useLingui() @@ -158,12 +159,12 @@ export function NotificationFeed({ () => isFetchingNextPage ? ( - + ) : ( ), - [isFetchingNextPage], + [isFetchingNextPage, t.palette.primary_500], ) useEffect(() => { diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx index 303e80b39..bf1c8b6b2 100644 --- a/src/view/com/notifications/NotificationFeedItem.tsx +++ b/src/view/com/notifications/NotificationFeedItem.tsx @@ -21,7 +21,9 @@ import { type AppBskyActorDefs, type AppBskyFeedDefs, AppBskyFeedPost, + type AppBskyGraphDefs, AppBskyGraphFollow, + AppBskyGraphStarterpack, moderateProfile, type ModerationDecision, type ModerationOpts, @@ -56,7 +58,12 @@ import {formatCount} from '#/view/com/util/numeric/format' import {TimeElapsed} from '#/view/com/util/TimeElapsed' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, native, platform, useTheme} from '#/alf' -import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import { + Button, + ButtonIcon, + ButtonText, + type ButtonContext, +} from '#/components/Button' import {FollowConfirmationDialog} from '#/components/dialogs/FollowConfirmationDialog' import {BellRinging_Filled_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging' import { @@ -80,13 +87,16 @@ import { } from '#/components/icons/Repost' import {StarterPack} from '#/components/icons/StarterPack' import {VerifiedCheck} from '#/components/icons/VerifiedCheck' -import {InlineLinkText, Link} from '#/components/Link' +import {InlineLinkText, Link, useLink} from '#/components/Link' import * as MediaPreview from '#/components/MediaPreview' import {ProfileBadges} from '#/components/ProfileBadges' import * as ProfileCard from '#/components/ProfileCard' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import * as Prompt from '#/components/Prompt' -import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard' +import { + Notification as StarterPackCard, + useStarterPackLink, +} from '#/components/StarterPack/StarterPackCard' import {SubtleHover} from '#/components/SubtleHover' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' @@ -263,35 +273,36 @@ let NotificationFeedItem = ({ } const firstAuthorLink = ( - - - - {forceLTR(firstAuthorName)} - - - - + + + {forceLTR(firstAuthorName)} + + + ) const additionalAuthorsCount = authors.length - 1 const hasMultipleAuthors = additionalAuthorsCount > 0 + const starterPack = item.notification.starterPack + const allFollowedViaSameStarterPack = + item.type === 'follow' && + starterPack !== undefined && + (item.additional ?? []).every( + notification => notification.starterPack?.uri === starterPack.uri, + ) + const starterPackName = + allFollowedViaSameStarterPack && starterPack + ? getStarterPackName(starterPack) + : undefined const formattedAuthorsCount = hasMultipleAuthors ? formatCount(i18n, additionalAuthorsCount) : '' @@ -370,17 +381,28 @@ let NotificationFeedItem = ({ * Follow-backs are ungrouped, grouped follow-backs not supported atm, * see `src/state/queries/notifications/util.ts` */ - a11yLabel = _(msg`${firstAuthorName} followed you back`) + a11yLabel = starterPackName + ? _(msg`${firstAuthorName} followed you back via starter pack ${starterPackName}`) + : _(msg`${firstAuthorName} followed you back`) notificationContent = {firstAuthorLink} followed you back } else { - a11yLabel = hasMultipleAuthors - ? _( - msg`${firstAuthorName} and ${plural(additionalAuthorsCount, { - one: `${formattedAuthorsCount} other`, - other: `${formattedAuthorsCount} others`, - })} followed you`, - ) - : _(msg`${firstAuthorName} followed you`) + a11yLabel = starterPackName + ? hasMultipleAuthors + ? _( + msg`${firstAuthorName} and ${plural(additionalAuthorsCount, { + one: `${formattedAuthorsCount} other`, + other: `${formattedAuthorsCount} others`, + })} followed you via starter pack ${starterPackName}`, + ) + : _(msg`${firstAuthorName} followed you via starter pack ${starterPackName}`) + : hasMultipleAuthors + ? _( + msg`${firstAuthorName} and ${plural(additionalAuthorsCount, { + one: `${formattedAuthorsCount} other`, + other: `${formattedAuthorsCount} others`, + })} followed you`, + ) + : _(msg`${firstAuthorName} followed you`) notificationContent = hasMultipleAuthors ? ( {firstAuthorLink} and{' '} @@ -603,35 +625,37 @@ let NotificationFeedItem = ({ return null } a11yLabel += ` ยท ${niceTimestamp}` + const isSingularFollow = item.type === 'follow' && !hasMultipleAuthors return ( - { - if (e.nativeEvent.actionName === 'activate') { - onBeforePress() - } - if (e.nativeEvent.actionName === 'toggleAuthorsExpanded') { - onToggleAuthorsExpanded() - } + ], + onAccessibilityAction: e => { + if (e.nativeEvent.actionName === 'activate') { + onBeforePress() + } + if (e.nativeEvent.actionName === 'toggleAuthorsExpanded') { + onToggleAuthorsExpanded() + } + }, }}> {({hovered}) => ( <> @@ -691,6 +715,9 @@ let NotificationFeedItem = ({ niceTimestamp={niceTimestamp} /> + {allFollowedViaSameStarterPack && starterPack ? ( + + ) : null} {(item.type === 'follow' && !hasMultipleAuthors && !isFollowBack) || (item.type === 'contact-match' && !item.notification.author.viewer?.following) ? ( @@ -737,12 +764,96 @@ let NotificationFeedItem = ({ )} - + ) } NotificationFeedItem = memo(NotificationFeedItem) export {NotificationFeedItem} +function NotificationRow({ + isSingularFollow, + linkProps, + children, +}: { + isSingularFollow: boolean + linkProps: Omit, 'children'> + children: (context: ButtonContext) => React.ReactElement +}) { + const {onPress} = useLink({ + to: linkProps.to, + displayText: '', + onPress: linkProps.onPress, + }) + + if (isSingularFollow) { + /* + * A Link renders as an anchor on web. Wrapping this row in one would nest + * the author's InlineLinkText inside that anchor, which browsers do not + * support. Let the profile link be the sole interactive target instead. + */ + return ( + + {children({hovered: false} as ButtonContext)} + + ) + } + + return {children} +} + +function FollowedViaStarterPack({ + starterPack, +}: { + starterPack: AppBskyGraphDefs.StarterPackViewBasic +}) { + const t = useTheme() + const link = useStarterPackLink({view: starterPack}) + const starterPackName = getStarterPackName(starterPack) + + if (!starterPackName) { + return null + } + + return ( + + + via starter pack{' '} + + { + e.stopPropagation() + link.precache() + }} + onMouseEnter={link.precache} + style={[a.font_semi_bold, t.atoms.text]}> + {starterPackName} + + + + ) +} + +function getStarterPackName( + starterPack: AppBskyGraphDefs.StarterPackViewBasic, +) { + return bsky.dangerousIsType( + starterPack.record, + AppBskyGraphStarterpack.isRecord, + ) + ? starterPack.record.name + : undefined +} + function NotificationContent({ content, timestamp,