Bluesky app fork with some witchin' additions 💫

[D1X] Add interstitials, component tweaks, placeholders (#4697)

* Add interstitials, component tweaks, placeholders

* Tweak feed card styles

* Port over same fix to ProfileCard

* Add browse more link on desktop

* Rm Gemfile

* Update logContext

* Update logContext

* Add click metric to cards

* Pass through props to ProfileCard.Link

* 2-up grid for profile cards on desktop web

* Add secondary_inverted button color

* Use inverted button color

* Adjust follow button layout

* Update skeleton

* Use round button

* Translate

authored by Eric Bailey and committed by GitHub 0598fc2f 6af78de9

Changed files
+564 -28
assets
src
+1
assets/icons/arrowRight_stroke2_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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" clip-rule="evenodd"/></svg>
+71
src/components/Button.tsx
··· 21 21 export type ButtonColor = 22 22 | 'primary' 23 23 | 'secondary' 24 + | 'secondary_inverted' 24 25 | 'negative' 25 26 | 'gradient_sky' 26 27 | 'gradient_midnight' ··· 235 236 }) 236 237 } 237 238 } 239 + } else if (color === 'secondary_inverted') { 240 + if (variant === 'solid') { 241 + if (!disabled) { 242 + baseStyles.push({ 243 + backgroundColor: t.palette.contrast_900, 244 + }) 245 + hoverStyles.push({ 246 + backgroundColor: t.palette.contrast_950, 247 + }) 248 + } else { 249 + baseStyles.push({ 250 + backgroundColor: t.palette.contrast_700, 251 + }) 252 + } 253 + } else if (variant === 'outline') { 254 + baseStyles.push(a.border, t.atoms.bg, { 255 + borderWidth: 1, 256 + }) 257 + 258 + if (!disabled) { 259 + baseStyles.push(a.border, { 260 + borderColor: t.palette.contrast_300, 261 + }) 262 + hoverStyles.push(t.atoms.bg_contrast_50) 263 + } else { 264 + baseStyles.push(a.border, { 265 + borderColor: t.palette.contrast_200, 266 + }) 267 + } 268 + } else if (variant === 'ghost') { 269 + if (!disabled) { 270 + baseStyles.push(t.atoms.bg) 271 + hoverStyles.push({ 272 + backgroundColor: t.palette.contrast_25, 273 + }) 274 + } 275 + } 238 276 } else if (color === 'negative') { 239 277 if (variant === 'solid') { 240 278 if (!disabled) { ··· 344 382 const gradient = { 345 383 primary: tokens.gradients.sky, 346 384 secondary: tokens.gradients.sky, 385 + secondary_inverted: tokens.gradients.sky, 347 386 negative: tokens.gradients.sky, 348 387 gradient_sky: tokens.gradients.sky, 349 388 gradient_midnight: tokens.gradients.midnight, ··· 472 511 if (!disabled) { 473 512 baseStyles.push({ 474 513 color: t.palette.contrast_700, 514 + }) 515 + } else { 516 + baseStyles.push({ 517 + color: t.palette.contrast_400, 518 + }) 519 + } 520 + } else if (variant === 'outline') { 521 + if (!disabled) { 522 + baseStyles.push({ 523 + color: t.palette.contrast_600, 524 + }) 525 + } else { 526 + baseStyles.push({ 527 + color: t.palette.contrast_300, 528 + }) 529 + } 530 + } else if (variant === 'ghost') { 531 + if (!disabled) { 532 + baseStyles.push({ 533 + color: t.palette.contrast_600, 534 + }) 535 + } else { 536 + baseStyles.push({ 537 + color: t.palette.contrast_300, 538 + }) 539 + } 540 + } 541 + } else if (color === 'secondary_inverted') { 542 + if (variant === 'solid' || variant === 'gradient') { 543 + if (!disabled) { 544 + baseStyles.push({ 545 + color: t.palette.white, 475 546 }) 476 547 } else { 477 548 baseStyles.push({
+31 -10
src/components/FeedCard.tsx
··· 30 30 import {Link as InternalLink, LinkProps} from '#/components/Link' 31 31 import {Loader} from '#/components/Loader' 32 32 import * as Prompt from '#/components/Prompt' 33 - import {RichText} from '#/components/RichText' 33 + import {RichText, RichTextProps} from '#/components/RichText' 34 34 import {Text} from '#/components/Typography' 35 35 36 36 type Props = { ··· 70 70 }, [view, queryClient]) 71 71 72 72 return ( 73 - <InternalLink to={href} {...props}> 73 + <InternalLink to={href} style={[a.flex_col]} {...props}> 74 74 {children} 75 75 </InternalLink> 76 76 ) 77 77 } 78 78 79 79 export function Outer({children}: {children: React.ReactNode}) { 80 - return <View style={[a.flex_1, a.gap_md]}>{children}</View> 80 + return <View style={[a.w_full, a.gap_md]}>{children}</View> 81 81 } 82 82 83 83 export function Header({children}: {children: React.ReactNode}) { 84 - return ( 85 - <View style={[a.flex_1, a.flex_row, a.align_center, a.gap_md]}> 86 - {children} 87 - </View> 88 - ) 84 + return <View style={[a.flex_row, a.align_center, a.gap_md]}>{children}</View> 89 85 } 90 86 91 87 export type AvatarProps = {src: string | undefined; size?: number} ··· 167 163 ) 168 164 } 169 165 170 - export function Description({description}: {description?: string}) { 166 + export function Description({ 167 + description, 168 + ...rest 169 + }: {description?: string} & Partial<RichTextProps>) { 171 170 const rt = React.useMemo(() => { 172 171 if (!description) return 173 172 const rt = new RichTextApi({text: description || ''}) ··· 175 174 return rt 176 175 }, [description]) 177 176 if (!rt) return null 178 - return <RichText value={rt} style={[a.leading_snug]} disableLinks /> 177 + return <RichText value={rt} style={[a.leading_snug]} disableLinks {...rest} /> 178 + } 179 + 180 + export function DescriptionPlaceholder() { 181 + const t = useTheme() 182 + return ( 183 + <View style={[a.gap_xs]}> 184 + <View 185 + style={[a.rounded_xs, a.w_full, t.atoms.bg_contrast_50, {height: 12}]} 186 + /> 187 + <View 188 + style={[a.rounded_xs, a.w_full, t.atoms.bg_contrast_50, {height: 12}]} 189 + /> 190 + <View 191 + style={[ 192 + a.rounded_xs, 193 + a.w_full, 194 + t.atoms.bg_contrast_50, 195 + {height: 12, width: 100}, 196 + ]} 197 + /> 198 + </View> 199 + ) 179 200 } 180 201 181 202 export function Likes({count}: {count: number}) {
+354
src/components/FeedInterstitials.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {ScrollView} from 'react-native-gesture-handler' 4 + import {AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api' 5 + import {msg, Trans} from '@lingui/macro' 6 + import {useLingui} from '@lingui/react' 7 + import {useNavigation} from '@react-navigation/native' 8 + 9 + import {NavigationProp} from '#/lib/routes/types' 10 + import {logEvent} from '#/lib/statsig/statsig' 11 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 12 + import {useGetPopularFeedsQuery} from '#/state/queries/feed' 13 + import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows' 14 + import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf' 15 + import {Button} from '#/components/Button' 16 + import * as FeedCard from '#/components/FeedCard' 17 + import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' 18 + import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' 19 + import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' 20 + import {InlineLinkText} from '#/components/Link' 21 + import * as ProfileCard from '#/components/ProfileCard' 22 + import {Text} from '#/components/Typography' 23 + 24 + function CardOuter({ 25 + children, 26 + style, 27 + }: {children: React.ReactNode | React.ReactNode[]} & ViewStyleProp) { 28 + const t = useTheme() 29 + const {gtMobile} = useBreakpoints() 30 + return ( 31 + <View 32 + style={[ 33 + a.w_full, 34 + a.p_lg, 35 + a.rounded_md, 36 + a.border, 37 + t.atoms.bg, 38 + t.atoms.border_contrast_low, 39 + !gtMobile && { 40 + width: 300, 41 + }, 42 + style, 43 + ]}> 44 + {children} 45 + </View> 46 + ) 47 + } 48 + 49 + export function SuggestedFollowPlaceholder() { 50 + const t = useTheme() 51 + return ( 52 + <CardOuter style={[a.gap_sm, t.atoms.border_contrast_low]}> 53 + <ProfileCard.Header> 54 + <ProfileCard.AvatarPlaceholder /> 55 + </ProfileCard.Header> 56 + 57 + <View style={[a.py_xs]}> 58 + <ProfileCard.NameAndHandlePlaceholder /> 59 + </View> 60 + 61 + <ProfileCard.DescriptionPlaceholder /> 62 + </CardOuter> 63 + ) 64 + } 65 + 66 + export function SuggestedFeedsCardPlaceholder() { 67 + const t = useTheme() 68 + return ( 69 + <CardOuter style={[a.gap_sm, t.atoms.border_contrast_low]}> 70 + <FeedCard.Header> 71 + <FeedCard.AvatarPlaceholder /> 72 + <FeedCard.TitleAndBylinePlaceholder creator /> 73 + </FeedCard.Header> 74 + 75 + <FeedCard.DescriptionPlaceholder /> 76 + </CardOuter> 77 + ) 78 + } 79 + 80 + export function SuggestedFollows() { 81 + const t = useTheme() 82 + const {_} = useLingui() 83 + const { 84 + isLoading: isSuggestionsLoading, 85 + data, 86 + error, 87 + } = useSuggestedFollowsQuery({limit: 6}) 88 + const moderationOpts = useModerationOpts() 89 + const navigation = useNavigation<NavigationProp>() 90 + const {gtMobile} = useBreakpoints() 91 + const isLoading = isSuggestionsLoading || !moderationOpts 92 + const maxLength = gtMobile ? 4 : 6 93 + 94 + const profiles: AppBskyActorDefs.ProfileViewBasic[] = [] 95 + if (data) { 96 + // Currently the responses contain duplicate items. 97 + // Needs to be fixed on backend, but let's dedupe to be safe. 98 + let seen = new Set() 99 + for (const page of data.pages) { 100 + for (const actor of page.actors) { 101 + if (!seen.has(actor.did)) { 102 + seen.add(actor.did) 103 + profiles.push(actor) 104 + } 105 + } 106 + } 107 + } 108 + 109 + const content = isLoading ? ( 110 + Array(maxLength) 111 + .fill(0) 112 + .map((_, i) => ( 113 + <View 114 + key={i} 115 + style={[gtMobile && web([a.flex_0, {width: 'calc(50% - 6px)'}])]}> 116 + <SuggestedFollowPlaceholder /> 117 + </View> 118 + )) 119 + ) : error || !profiles.length ? null : ( 120 + <> 121 + {profiles.slice(0, maxLength).map(profile => ( 122 + <ProfileCard.Link 123 + key={profile.did} 124 + did={profile.handle} 125 + onPress={() => { 126 + logEvent('feed:interstitial:profileCard:press', {}) 127 + }} 128 + style={[ 129 + a.flex_1, 130 + gtMobile && web([a.flex_0, {width: 'calc(50% - 6px)'}]), 131 + ]}> 132 + {({hovered, pressed}) => ( 133 + <CardOuter 134 + style={[ 135 + a.flex_1, 136 + (hovered || pressed) && t.atoms.border_contrast_high, 137 + ]}> 138 + <ProfileCard.Outer> 139 + <ProfileCard.Header> 140 + <ProfileCard.Avatar 141 + profile={profile} 142 + moderationOpts={moderationOpts} 143 + /> 144 + <ProfileCard.NameAndHandle 145 + profile={profile} 146 + moderationOpts={moderationOpts} 147 + /> 148 + <ProfileCard.FollowButton 149 + profile={profile} 150 + logContext="FeedInterstitial" 151 + color="secondary_inverted" 152 + shape="round" 153 + /> 154 + </ProfileCard.Header> 155 + <ProfileCard.Description profile={profile} /> 156 + </ProfileCard.Outer> 157 + </CardOuter> 158 + )} 159 + </ProfileCard.Link> 160 + ))} 161 + </> 162 + ) 163 + 164 + return error ? null : ( 165 + <View 166 + style={[a.border_t, t.atoms.border_contrast_low, t.atoms.bg_contrast_25]}> 167 + <View style={[a.pt_2xl, a.px_lg, a.flex_row, a.pb_xs]}> 168 + <Text 169 + style={[ 170 + a.flex_1, 171 + a.text_lg, 172 + a.font_bold, 173 + t.atoms.text_contrast_medium, 174 + ]}> 175 + <Trans>Suggested for you</Trans> 176 + </Text> 177 + <Person fill={t.atoms.text_contrast_low.color} /> 178 + </View> 179 + 180 + {gtMobile ? ( 181 + <View style={[a.flex_1, a.px_lg, a.pt_md, a.pb_xl, a.gap_md]}> 182 + <View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_md]}> 183 + {content} 184 + </View> 185 + 186 + <View 187 + style={[ 188 + a.flex_row, 189 + a.justify_end, 190 + a.align_center, 191 + a.pt_xs, 192 + a.gap_md, 193 + ]}> 194 + <InlineLinkText to="/search" style={[t.atoms.text_contrast_medium]}> 195 + <Trans>Browse more suggestions</Trans> 196 + </InlineLinkText> 197 + <Arrow size="sm" fill={t.atoms.text_contrast_medium.color} /> 198 + </View> 199 + </View> 200 + ) : ( 201 + <ScrollView horizontal showsHorizontalScrollIndicator={false}> 202 + <View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}> 203 + {content} 204 + 205 + <Button 206 + label={_(msg`Browse more accounts on our explore page`)} 207 + onPress={() => { 208 + navigation.navigate('SearchTab') 209 + }}> 210 + <CardOuter style={[a.flex_1, {borderWidth: 0}]}> 211 + <View style={[a.flex_1, a.justify_center]}> 212 + <View style={[a.flex_row, a.px_lg]}> 213 + <Text style={[a.pr_xl, a.flex_1, a.leading_snug]}> 214 + <Trans>Browse more suggestions on our explore page</Trans> 215 + </Text> 216 + 217 + <Arrow size="xl" /> 218 + </View> 219 + </View> 220 + </CardOuter> 221 + </Button> 222 + </View> 223 + </ScrollView> 224 + )} 225 + </View> 226 + ) 227 + } 228 + 229 + export function SuggestedFeeds() { 230 + const numFeedsToDisplay = 3 231 + const t = useTheme() 232 + const {_} = useLingui() 233 + const {data, isLoading, error} = useGetPopularFeedsQuery({ 234 + limit: numFeedsToDisplay, 235 + }) 236 + const navigation = useNavigation<NavigationProp>() 237 + const {gtMobile} = useBreakpoints() 238 + 239 + const feeds = React.useMemo(() => { 240 + const items: AppBskyFeedDefs.GeneratorView[] = [] 241 + 242 + if (!data) return items 243 + 244 + for (const page of data.pages) { 245 + for (const feed of page.feeds) { 246 + items.push(feed) 247 + } 248 + } 249 + 250 + return items 251 + }, [data]) 252 + 253 + const content = isLoading ? ( 254 + Array(numFeedsToDisplay) 255 + .fill(0) 256 + .map((_, i) => <SuggestedFeedsCardPlaceholder key={i} />) 257 + ) : error || !feeds ? null : ( 258 + <> 259 + {feeds.slice(0, numFeedsToDisplay).map(feed => ( 260 + <FeedCard.Link 261 + key={feed.uri} 262 + view={feed} 263 + onPress={() => { 264 + logEvent('feed:interstitial:feedCard:press', {}) 265 + }}> 266 + {({hovered, pressed}) => ( 267 + <CardOuter 268 + style={[ 269 + a.flex_1, 270 + (hovered || pressed) && t.atoms.border_contrast_high, 271 + ]}> 272 + <FeedCard.Outer> 273 + <FeedCard.Header> 274 + <FeedCard.Avatar src={feed.avatar} /> 275 + <FeedCard.TitleAndByline 276 + title={feed.displayName} 277 + creator={feed.creator} 278 + /> 279 + </FeedCard.Header> 280 + <FeedCard.Description 281 + description={feed.description} 282 + numberOfLines={3} 283 + /> 284 + </FeedCard.Outer> 285 + </CardOuter> 286 + )} 287 + </FeedCard.Link> 288 + ))} 289 + </> 290 + ) 291 + 292 + return error ? null : ( 293 + <View 294 + style={[a.border_t, t.atoms.border_contrast_low, t.atoms.bg_contrast_25]}> 295 + <View style={[a.pt_2xl, a.px_lg, a.flex_row, a.pb_xs]}> 296 + <Text 297 + style={[ 298 + a.flex_1, 299 + a.text_lg, 300 + a.font_bold, 301 + t.atoms.text_contrast_medium, 302 + ]}> 303 + <Trans>Some other feeds you might like</Trans> 304 + </Text> 305 + <Hashtag fill={t.atoms.text_contrast_low.color} /> 306 + </View> 307 + 308 + {gtMobile ? ( 309 + <View style={[a.flex_1, a.px_lg, a.pt_md, a.pb_xl, a.gap_md]}> 310 + {content} 311 + 312 + <View 313 + style={[ 314 + a.flex_row, 315 + a.justify_end, 316 + a.align_center, 317 + a.pt_xs, 318 + a.gap_md, 319 + ]}> 320 + <InlineLinkText to="/search" style={[t.atoms.text_contrast_medium]}> 321 + <Trans>Browse more suggestions</Trans> 322 + </InlineLinkText> 323 + <Arrow size="sm" fill={t.atoms.text_contrast_medium.color} /> 324 + </View> 325 + </View> 326 + ) : ( 327 + <ScrollView horizontal showsHorizontalScrollIndicator={false}> 328 + <View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}> 329 + {content} 330 + 331 + <Button 332 + label={_(msg`Browse more feeds on our explore page`)} 333 + onPress={() => { 334 + navigation.navigate('SearchTab') 335 + }} 336 + style={[a.flex_col]}> 337 + <CardOuter style={[a.flex_1]}> 338 + <View style={[a.flex_1, a.justify_center]}> 339 + <View style={[a.flex_row, a.px_lg]}> 340 + <Text style={[a.pr_xl, a.flex_1, a.leading_snug]}> 341 + <Trans>Browse more suggestions on our explore page</Trans> 342 + </Text> 343 + 344 + <Arrow size="xl" /> 345 + </View> 346 + </View> 347 + </CardOuter> 348 + </Button> 349 + </View> 350 + </ScrollView> 351 + )} 352 + </View> 353 + ) 354 + }
+82 -5
src/components/ProfileCard.tsx
··· 9 9 import {msg} from '@lingui/macro' 10 10 import {useLingui} from '@lingui/react' 11 11 12 + import {LogEvents} from '#/lib/statsig/statsig' 12 13 import {sanitizeDisplayName} from '#/lib/strings/display-names' 13 14 import {useProfileFollowMutationQueue} from '#/state/queries/profile' 14 15 import {sanitizeHandle} from 'lib/strings/handles' ··· 79 80 }: { 80 81 children: React.ReactElement | React.ReactElement[] 81 82 }) { 82 - return <View style={[a.flex_1, a.gap_xs]}>{children}</View> 83 + return <View style={[a.w_full, a.flex_1, a.gap_xs]}>{children}</View> 83 84 } 84 85 85 86 export function Header({ ··· 87 88 }: { 88 89 children: React.ReactElement | React.ReactElement[] 89 90 }) { 90 - return <View style={[a.flex_row, a.gap_sm]}>{children}</View> 91 + return <View style={[a.flex_row, a.align_center, a.gap_sm]}>{children}</View> 91 92 } 92 93 93 - export function Link({did, children}: {did: string} & Omit<LinkProps, 'to'>) { 94 + export function Link({ 95 + did, 96 + children, 97 + style, 98 + ...rest 99 + }: {did: string} & Omit<LinkProps, 'to'>) { 94 100 return ( 95 101 <InternalLink 96 102 to={{ 97 103 screen: 'Profile', 98 104 params: {name: did}, 99 - }}> 105 + }} 106 + style={[a.flex_col, style]} 107 + {...rest}> 100 108 {children} 101 109 </InternalLink> 102 110 ) ··· 121 129 ) 122 130 } 123 131 132 + export function AvatarPlaceholder() { 133 + const t = useTheme() 134 + return ( 135 + <View 136 + style={[ 137 + a.rounded_full, 138 + t.atoms.bg_contrast_50, 139 + { 140 + width: 42, 141 + height: 42, 142 + }, 143 + ]} 144 + /> 145 + ) 146 + } 147 + 124 148 export function NameAndHandle({ 125 149 profile, 126 150 moderationOpts, ··· 150 174 ) 151 175 } 152 176 177 + export function NameAndHandlePlaceholder() { 178 + const t = useTheme() 179 + 180 + return ( 181 + <View style={[a.flex_1, a.gap_xs]}> 182 + <View 183 + style={[ 184 + a.rounded_xs, 185 + t.atoms.bg_contrast_50, 186 + { 187 + width: '60%', 188 + height: 14, 189 + }, 190 + ]} 191 + /> 192 + 193 + <View 194 + style={[ 195 + a.rounded_xs, 196 + t.atoms.bg_contrast_50, 197 + { 198 + width: '40%', 199 + height: 10, 200 + }, 201 + ]} 202 + /> 203 + </View> 204 + ) 205 + } 206 + 153 207 export function Description({ 154 208 profile: profileUnshadowed, 155 209 }: { ··· 183 237 ) 184 238 } 185 239 240 + export function DescriptionPlaceholder() { 241 + const t = useTheme() 242 + return ( 243 + <View style={[a.gap_xs]}> 244 + <View 245 + style={[a.rounded_xs, a.w_full, t.atoms.bg_contrast_50, {height: 12}]} 246 + /> 247 + <View 248 + style={[a.rounded_xs, a.w_full, t.atoms.bg_contrast_50, {height: 12}]} 249 + /> 250 + <View 251 + style={[ 252 + a.rounded_xs, 253 + a.w_full, 254 + t.atoms.bg_contrast_50, 255 + {height: 12, width: 100}, 256 + ]} 257 + /> 258 + </View> 259 + ) 260 + } 261 + 186 262 export type FollowButtonProps = { 187 263 profile: AppBskyActorDefs.ProfileViewBasic 188 - logContext: 'ProfileCard' | 'StarterPackProfilesList' 264 + logContext: LogEvents['profile:follow']['logContext'] & 265 + LogEvents['profile:unfollow']['logContext'] 189 266 } & Partial<ButtonProps> 190 267 191 268 export function FollowButton(props: FollowButtonProps) {
+14 -12
src/components/RichText.tsx
··· 17 17 18 18 const WORD_WRAP = {wordWrap: 1} 19 19 20 + export type RichTextProps = TextStyleProp & 21 + Pick<TextProps, 'selectable'> & { 22 + value: RichTextAPI | string 23 + testID?: string 24 + numberOfLines?: number 25 + disableLinks?: boolean 26 + enableTags?: boolean 27 + authorHandle?: string 28 + onLinkPress?: LinkProps['onPress'] 29 + interactiveStyle?: TextStyle 30 + emojiMultiplier?: number 31 + } 32 + 20 33 export function RichText({ 21 34 testID, 22 35 value, ··· 29 42 onLinkPress, 30 43 interactiveStyle, 31 44 emojiMultiplier = 1.85, 32 - }: TextStyleProp & 33 - Pick<TextProps, 'selectable'> & { 34 - value: RichTextAPI | string 35 - testID?: string 36 - numberOfLines?: number 37 - disableLinks?: boolean 38 - enableTags?: boolean 39 - authorHandle?: string 40 - onLinkPress?: LinkProps['onPress'] 41 - interactiveStyle?: TextStyle 42 - emojiMultiplier?: number 43 - }) { 45 + }: RichTextProps) { 44 46 const richText = React.useMemo( 45 47 () => 46 48 value instanceof RichTextAPI ? value : new RichTextAPI({text: value}),
+4
src/components/icons/Arrow.tsx
··· 8 8 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', 9 9 }) 10 10 11 + export const ArrowRight_Stroke2_Corner0_Rounded = createSinglePathSVG({ 12 + 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', 13 + }) 14 + 11 15 export const ArrowBottom_Stroke2_Corner0_Rounded = createSinglePathSVG({ 12 16 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', 13 17 })
+5
src/lib/statsig/events.ts
··· 153 153 | 'ProfileHoverCard' 154 154 | 'AvatarButton' 155 155 | 'StarterPackProfilesList' 156 + | 'FeedInterstitial' 156 157 } 157 158 'profile:unfollow': { 158 159 logContext: ··· 166 167 | 'Chat' 167 168 | 'AvatarButton' 168 169 | 'StarterPackProfilesList' 170 + | 'FeedInterstitial' 169 171 } 170 172 'chat:create': { 171 173 logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog' ··· 200 202 'starterPack:opened': { 201 203 starterPack: string 202 204 } 205 + 206 + 'feed:interstitial:profileCard:press': {} 207 + 'feed:interstitial:feedCard:press': {} 203 208 204 209 'test:all:always': {} 205 210 'test:all:sometimes': {}
+1
src/view/screens/Feeds.tsx
··· 642 642 const t = useTheme() 643 643 644 644 const commonStyle = [ 645 + a.w_full, 645 646 a.flex_1, 646 647 a.px_lg, 647 648 a.py_md,
+1 -1
src/view/screens/Storybook/Buttons.tsx
··· 20 20 <H1>Buttons</H1> 21 21 22 22 <View style={[a.flex_row, a.flex_wrap, a.gap_md, a.align_start]}> 23 - {['primary', 'secondary', 'negative'].map(color => ( 23 + {['primary', 'secondary', 'secondary_inverted'].map(color => ( 24 24 <View key={color} style={[a.gap_md, a.align_start]}> 25 25 {['solid', 'outline', 'ghost'].map(variant => ( 26 26 <React.Fragment key={variant}>