From 74e076b5e43e548332a8ba4f4968df0a9b24d3c5 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 28 Jun 2024 10:25:08 -0500 Subject: [PATCH] Add interstitials, component tweaks, placeholders --- .../arrowRight_stroke2_corner0_rounded.svg | 1 + src/components/FeedCard.tsx | 39 ++- src/components/FeedInterstitials.tsx | 298 ++++++++++++++++++ src/components/ProfileCard.tsx | 77 ++++- src/components/RichText.tsx | 26 +- src/components/icons/Arrow.tsx | 4 + src/lib/statsig/events.ts | 2 + 7 files changed, 423 insertions(+), 24 deletions(-) create mode 100644 assets/icons/arrowRight_stroke2_corner0_rounded.svg create mode 100644 src/components/FeedInterstitials.tsx diff --git a/assets/icons/arrowRight_stroke2_corner0_rounded.svg b/assets/icons/arrowRight_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..dbbbbc2c12 --- /dev/null +++ b/assets/icons/arrowRight_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx index b1200d9c4e..7c13df7ee1 100644 --- a/src/components/FeedCard.tsx +++ b/src/components/FeedCard.tsx @@ -30,7 +30,7 @@ import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' import {Link as InternalLink, LinkProps} from '#/components/Link' import {Loader} from '#/components/Loader' import * as Prompt from '#/components/Prompt' -import {RichText} from '#/components/RichText' +import {RichText, RichTextProps} from '#/components/RichText' import {Text} from '#/components/Typography' type Props = { @@ -70,7 +70,7 @@ export function Link({ }, [view, queryClient]) return ( - + {children} ) @@ -81,11 +81,7 @@ export function Outer({children}: {children: React.ReactNode}) { } export function Header({children}: {children: React.ReactNode}) { - return ( - - {children} - - ) + return {children} } export type AvatarProps = {src: string | undefined; size?: number} @@ -167,7 +163,10 @@ export function TitleAndBylinePlaceholder({creator}: {creator?: boolean}) { ) } -export function Description({description}: {description?: string}) { +export function Description({ + description, + ...rest +}: {description?: string} & Partial) { const rt = React.useMemo(() => { if (!description) return const rt = new RichTextApi({text: description || ''}) @@ -175,7 +174,29 @@ export function Description({description}: {description?: string}) { return rt }, [description]) if (!rt) return null - return + return +} + +export function DescriptionPlaceholder() { + const t = useTheme() + return ( + + + + + + ) } export function Likes({count}: {count: number}) { diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx new file mode 100644 index 0000000000..56a2dbb256 --- /dev/null +++ b/src/components/FeedInterstitials.tsx @@ -0,0 +1,298 @@ +import React from 'react' +import {View} from 'react-native' +import {ScrollView} from 'react-native-gesture-handler' +import {AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' + +import {NavigationProp} from '#/lib/routes/types' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useGetPopularFeedsQuery} from '#/state/queries/feed' +import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows' +import {atoms as a, useBreakpoints, useTheme, ViewStyleProp} from '#/alf' +import {Button} from '#/components/Button' +import * as FeedCard from '#/components/FeedCard' +import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' +import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' +import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' +import * as ProfileCard from '#/components/ProfileCard' +import {Text} from '#/components/Typography' + +function CardOuter({ + children, + style, +}: {children: React.ReactNode | React.ReactNode[]} & ViewStyleProp) { + const t = useTheme() + const {gtMobile} = useBreakpoints() + return ( + + {children} + + ) +} + +export function SuggestedFollowPlaceholder() { + const t = useTheme() + return ( + + + + + + + + + ) +} + +export function SuggestedFeedsCardPlaceholder() { + const t = useTheme() + return ( + + + + + + + + + ) +} + +export function SuggestedFollows() { + const t = useTheme() + const {_} = useLingui() + const { + isLoading: isSuggestionsLoading, + data, + error, + } = useSuggestedFollowsQuery({limit: 6}) + const moderationOpts = useModerationOpts() + const navigation = useNavigation() + const {gtMobile} = useBreakpoints() + const isLoading = isSuggestionsLoading || !moderationOpts + const maxLength = gtMobile ? 3 : 6 + + const profiles: AppBskyActorDefs.ProfileViewBasic[] = [] + if (data) { + // Currently the responses contain duplicate items. + // Needs to be fixed on backend, but let's dedupe to be safe. + let seen = new Set() + for (const page of data.pages) { + for (const actor of page.actors) { + if (!seen.has(actor.did)) { + seen.add(actor.did) + profiles.push(actor) + } + } + } + } + + const content = isLoading ? ( + Array(3) + .fill(0) + .map((_, i) => ) + ) : error || !profiles.length ? null : ( + <> + {profiles.slice(0, maxLength).map(profile => ( + + {({hovered, pressed}) => ( + + + + + + + + + + + )} + + ))} + + ) + + return error ? null : ( + + + + Suggested for you + + + + + {gtMobile ? ( + + {content} + + ) : ( + + + {content} + + + + + )} + + ) +} + +export function SuggestedFeeds() { + const numFeedsToDisplay = 3 + const t = useTheme() + const {_} = useLingui() + const {data, isLoading, error} = useGetPopularFeedsQuery({ + limit: numFeedsToDisplay, + }) + const navigation = useNavigation() + const {gtMobile} = useBreakpoints() + + const feeds = React.useMemo(() => { + const items: AppBskyFeedDefs.GeneratorView[] = [] + + if (!data) return items + + for (const page of data.pages) { + for (const feed of page.feeds) { + items.push(feed) + } + } + + return items + }, [data]) + + const content = isLoading ? ( + Array(numFeedsToDisplay) + .fill(0) + .map((_, i) => ) + ) : error || !feeds ? null : ( + <> + {feeds.slice(0, numFeedsToDisplay).map(feed => ( + + {({hovered, pressed}) => ( + + + + + + + + + + )} + + ))} + + ) + + return error ? null : ( + + + + Some other feeds you might like + + + + + {gtMobile ? ( + + {content} + + ) : ( + + + {content} + + + + + )} + + ) +} diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index a6ca7627b2..ce6e6ffe8b 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -9,6 +9,7 @@ import { import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {LogEvents} from '#/lib/statsig/statsig' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {useProfileFollowMutationQueue} from '#/state/queries/profile' import {sanitizeHandle} from 'lib/strings/handles' @@ -87,7 +88,7 @@ export function Header({ }: { children: React.ReactElement | React.ReactElement[] }) { - return {children} + return {children} } export function Link({did, children}: {did: string} & Omit) { @@ -96,7 +97,8 @@ export function Link({did, children}: {did: string} & Omit) { to={{ screen: 'Profile', params: {name: did}, - }}> + }} + style={[a.flex_col]}> {children} ) @@ -121,6 +123,22 @@ export function Avatar({ ) } +export function AvatarPlaceholder() { + const t = useTheme() + return ( + + ) +} + export function NameAndHandle({ profile, moderationOpts, @@ -150,6 +168,36 @@ export function NameAndHandle({ ) } +export function NameAndHandlePlaceholder() { + const t = useTheme() + + return ( + + + + + + ) +} + export function Description({ profile: profileUnshadowed, }: { @@ -183,9 +231,32 @@ export function Description({ ) } +export function DescriptionPlaceholder() { + const t = useTheme() + return ( + + + + + + ) +} + export type FollowButtonProps = { profile: AppBskyActorDefs.ProfileViewBasic - logContext: 'ProfileCard' | 'StarterPackProfilesList' + logContext: LogEvents['profile:follow']['logContext'] & + LogEvents['profile:unfollow']['logContext'] } & Partial export function FollowButton(props: FollowButtonProps) { diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx index 9ba44eabe4..7511775978 100644 --- a/src/components/RichText.tsx +++ b/src/components/RichText.tsx @@ -17,6 +17,19 @@ import {Text, TextProps} from '#/components/Typography' const WORD_WRAP = {wordWrap: 1} +export type RichTextProps = TextStyleProp & + Pick & { + value: RichTextAPI | string + testID?: string + numberOfLines?: number + disableLinks?: boolean + enableTags?: boolean + authorHandle?: string + onLinkPress?: LinkProps['onPress'] + interactiveStyle?: TextStyle + emojiMultiplier?: number + } + export function RichText({ testID, value, @@ -29,18 +42,7 @@ export function RichText({ onLinkPress, interactiveStyle, emojiMultiplier = 1.85, -}: TextStyleProp & - Pick & { - value: RichTextAPI | string - testID?: string - numberOfLines?: number - disableLinks?: boolean - enableTags?: boolean - authorHandle?: string - onLinkPress?: LinkProps['onPress'] - interactiveStyle?: TextStyle - emojiMultiplier?: number - }) { +}: RichTextProps) { const richText = React.useMemo( () => value instanceof RichTextAPI ? value : new RichTextAPI({text: value}), diff --git a/src/components/icons/Arrow.tsx b/src/components/icons/Arrow.tsx index d6fb635e96..0d4bc9479e 100644 --- a/src/components/icons/Arrow.tsx +++ b/src/components/icons/Arrow.tsx @@ -8,6 +8,10 @@ export const ArrowLeft_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M3 12a1 1 0 0 1 .293-.707l6-6a1 1 0 0 1 1.414 1.414L6.414 11H20a1 1 0 1 1 0 2H6.414l4.293 4.293a1 1 0 0 1-1.414 1.414l-6-6A1 1 0 0 1 3 12Z', }) +export const ArrowRight_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M21 12a1 1 0 0 1-.293.707l-6 6a1 1 0 0 1-1.414-1.414L17.586 13H4a1 1 0 1 1 0-2h13.586l-4.293-4.293a1 1 0 0 1 1.414-1.414l6 6A1 1 0 0 1 21 12Z', +}) + export const ArrowBottom_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M12 21a1 1 0 0 1-.707-.293l-6-6a1 1 0 1 1 1.414-1.414L11 17.586V4a1 1 0 1 1 2 0v13.586l4.293-4.293a1 1 0 0 1 1.414 1.414l-6 6A1 1 0 0 1 12 21Z', }) diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index 3efc11a51f..2eb921b094 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -147,6 +147,7 @@ export type LogEvents = { | 'ProfileHoverCard' | 'AvatarButton' | 'StarterPackProfilesList' + | 'FeedSuggestedFollowsCard' } 'profile:unfollow': { logContext: @@ -160,6 +161,7 @@ export type LogEvents = { | 'Chat' | 'AvatarButton' | 'StarterPackProfilesList' + | 'FeedSuggestedFollowsCard' } 'chat:create': { logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog'