Bluesky app fork with some witchin' additions 💫

Improve "replied to a post" component (#8602)

* unify component

* change bottom padding from 2px to 4px

authored by samuel.fm and committed by

GitHub 221623f5 f4dca5d2

+110 -158
+1
assets/icons/arrowCornerDownRight_stroke2_rounded_2_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M15.793 10.293a1 1 0 0 1 1.338-.068l.076.068 3.293 3.293a2 2 0 0 1 .138 2.677l-.138.151-3.293 3.293a1 1 0 1 1-1.414-1.414L18.086 16H8a5 5 0 0 1-5-5V5a1 1 0 0 1 2 0v6a3 3 0 0 0 3 3h10.086l-2.293-2.293-.068-.076a1 1 0 0 1 .068-1.338Z"/></svg>
+63
src/components/Post/PostRepliedTo.tsx
··· 1 + import {View} from 'react-native' 2 + import {Trans} from '@lingui/macro' 3 + 4 + import {useSession} from '#/state/session' 5 + import {UserInfoText} from '#/view/com/util/UserInfoText' 6 + import {atoms as a, useTheme} from '#/alf' 7 + import {ArrowCornerDownRight_Stroke2_Corner2_Rounded as ArrowCornerDownRightIcon} from '#/components/icons/ArrowCornerDownRight' 8 + import {ProfileHoverCard} from '#/components/ProfileHoverCard' 9 + import {Text} from '#/components/Typography' 10 + import type * as bsky from '#/types/bsky' 11 + 12 + export function PostRepliedTo({ 13 + parentAuthor, 14 + isParentBlocked, 15 + isParentNotFound, 16 + }: { 17 + parentAuthor: string | bsky.profile.AnyProfileView | undefined 18 + isParentBlocked?: boolean 19 + isParentNotFound?: boolean 20 + }) { 21 + const t = useTheme() 22 + const {currentAccount} = useSession() 23 + 24 + const textStyle = [a.text_sm, t.atoms.text_contrast_medium, a.leading_snug] 25 + 26 + let label 27 + if (isParentBlocked) { 28 + label = <Trans context="description">Replied to a blocked post</Trans> 29 + } else if (isParentNotFound) { 30 + label = <Trans context="description">Replied to a post</Trans> 31 + } else if (parentAuthor) { 32 + const did = 33 + typeof parentAuthor === 'string' ? parentAuthor : parentAuthor.did 34 + const isMe = currentAccount?.did === did 35 + if (isMe) { 36 + label = <Trans context="description">Replied to you</Trans> 37 + } else { 38 + label = ( 39 + <Trans context="description"> 40 + Replied to{' '} 41 + <ProfileHoverCard did={did}> 42 + <UserInfoText did={did} attr="displayName" style={textStyle} /> 43 + </ProfileHoverCard> 44 + </Trans> 45 + ) 46 + } 47 + } 48 + 49 + if (!label) { 50 + // Should not happen. 51 + return null 52 + } 53 + 54 + return ( 55 + <View style={[a.flex_row, a.align_center, a.pb_xs, a.gap_xs]}> 56 + <ArrowCornerDownRightIcon 57 + size="xs" 58 + style={[t.atoms.text_contrast_medium, {top: -1}]} 59 + /> 60 + <Text style={textStyle}>{label}</Text> 61 + </View> 62 + ) 63 + }
+7
src/components/icons/ArrowCornerDownRight.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const ArrowCornerDownRight_Stroke2_Corner2_Rounded = createSinglePathSVG( 4 + { 5 + path: 'M15.793 10.293a1 1 0 0 1 1.338-.068l.076.068 3.293 3.293a2 2 0 0 1 .138 2.677l-.138.151-3.293 3.293a1 1 0 1 1-1.414-1.414L18.086 16H8a5 5 0 0 1-5-5V5a1 1 0 0 1 2 0v6a3 3 0 0 0 3 3h10.086l-2.293-2.293-.068-.076a1 1 0 0 1 .068-1.338Z', 6 + }, 7 + )
+5 -40
src/view/com/post/Post.tsx
··· 8 8 type ModerationDecision, 9 9 RichText as RichTextAPI, 10 10 } from '@atproto/api' 11 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 12 - import {Trans} from '@lingui/macro' 13 11 import {useQueryClient} from '@tanstack/react-query' 14 12 15 13 import {MAX_POST_LINES} from '#/lib/constants' ··· 17 15 import {usePalette} from '#/lib/hooks/usePalette' 18 16 import {makeProfileLink} from '#/lib/routes/links' 19 17 import {countLines} from '#/lib/strings/helpers' 20 - import {colors, s} from '#/lib/styles' 18 + import {colors} from '#/lib/styles' 21 19 import { 22 20 POST_TOMBSTONE, 23 21 type Shadow, 24 22 usePostShadow, 25 23 } from '#/state/cache/post-shadow' 26 24 import {useModerationOpts} from '#/state/preferences/moderation-opts' 27 - import {precacheProfile} from '#/state/queries/profile' 28 - import {useSession} from '#/state/session' 25 + import {unstableCacheProfileView} from '#/state/queries/profile' 29 26 import {Link} from '#/view/com/util/Link' 30 27 import {PostMeta} from '#/view/com/util/PostMeta' 31 - import {Text} from '#/view/com/util/text/Text' 32 28 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 33 - import {UserInfoText} from '#/view/com/util/UserInfoText' 34 29 import {atoms as a} from '#/alf' 35 30 import {ContentHider} from '#/components/moderation/ContentHider' 36 31 import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' 37 32 import {PostAlerts} from '#/components/moderation/PostAlerts' 38 33 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 34 + import {PostRepliedTo} from '#/components/Post/PostRepliedTo' 39 35 import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' 40 36 import {PostControls} from '#/components/PostControls' 41 - import {ProfileHoverCard} from '#/components/ProfileHoverCard' 42 37 import {RichText} from '#/components/RichText' 43 38 import {SubtleWebHover} from '#/components/SubtleWebHover' 44 39 import * as bsky from '#/types/bsky' ··· 145 140 }, [setLimitLines]) 146 141 147 142 const onBeforePress = useCallback(() => { 148 - precacheProfile(queryClient, post.author) 143 + unstableCacheProfileView(queryClient, post.author) 149 144 }, [queryClient, post.author]) 150 - 151 - const {currentAccount} = useSession() 152 - const isMe = replyAuthorDid === currentAccount?.did 153 145 154 146 const [hover, setHover] = useState(false) 155 147 return ( ··· 187 179 postHref={itemHref} 188 180 /> 189 181 {replyAuthorDid !== '' && ( 190 - <View style={[s.flexRow, s.mb2, s.alignCenter]}> 191 - <FontAwesomeIcon 192 - icon="reply" 193 - size={9} 194 - style={[pal.textLight, s.mr5]} 195 - /> 196 - <Text 197 - type="sm" 198 - style={[pal.textLight, s.mr2]} 199 - lineHeight={1.2} 200 - numberOfLines={1}> 201 - {isMe ? ( 202 - <Trans context="description">Reply to you</Trans> 203 - ) : ( 204 - <Trans context="description"> 205 - Reply to{' '} 206 - <ProfileHoverCard did={replyAuthorDid}> 207 - <UserInfoText 208 - type="sm" 209 - did={replyAuthorDid} 210 - attr="displayName" 211 - style={[pal.textLight]} 212 - /> 213 - </ProfileHoverCard> 214 - </Trans> 215 - )} 216 - </Text> 217 - </View> 182 + <PostRepliedTo parentAuthor={replyAuthorDid} /> 218 183 )} 219 184 <LabelsOnMyPost post={post} /> 220 185 <ContentHider
+5 -79
src/view/com/posts/PostFeedItem.tsx
··· 9 9 type ModerationDecision, 10 10 RichText as RichTextAPI, 11 11 } from '@atproto/api' 12 - import { 13 - FontAwesomeIcon, 14 - type FontAwesomeIconStyle, 15 - } from '@fortawesome/react-native-fontawesome' 16 12 import {msg, Trans} from '@lingui/macro' 17 13 import {useLingui} from '@lingui/react' 18 14 import {useQueryClient} from '@tanstack/react-query' ··· 26 22 import {sanitizeDisplayName} from '#/lib/strings/display-names' 27 23 import {sanitizeHandle} from '#/lib/strings/handles' 28 24 import {countLines} from '#/lib/strings/helpers' 29 - import {s} from '#/lib/styles' 30 25 import { 31 26 POST_TOMBSTONE, 32 27 type Shadow, ··· 54 49 import {type AppModerationCause} from '#/components/Pills' 55 50 import {Embed} from '#/components/Post/Embed' 56 51 import {PostEmbedViewContext} from '#/components/Post/Embed/types' 52 + import {PostRepliedTo} from '#/components/Post/PostRepliedTo' 57 53 import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' 58 54 import {PostControls} from '#/components/PostControls' 59 55 import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug' ··· 448 444 /> 449 445 {showReplyTo && 450 446 (parentAuthor || isParentBlocked || isParentNotFound) && ( 451 - <ReplyToLabel 452 - blocked={isParentBlocked} 453 - notFound={isParentNotFound} 454 - profile={parentAuthor} 447 + <PostRepliedTo 448 + parentAuthor={parentAuthor} 449 + isParentBlocked={isParentBlocked} 450 + isParentNotFound={isParentNotFound} 455 451 /> 456 452 )} 457 453 <LabelsOnMyPost post={post} /> ··· 576 572 } 577 573 PostContent = memo(PostContent) 578 574 579 - function ReplyToLabel({ 580 - profile, 581 - blocked, 582 - notFound, 583 - }: { 584 - profile: AppBskyActorDefs.ProfileViewBasic | undefined 585 - blocked?: boolean 586 - notFound?: boolean 587 - }) { 588 - const pal = usePalette('default') 589 - const {currentAccount} = useSession() 590 - 591 - let label 592 - if (blocked) { 593 - label = <Trans context="description">Reply to a blocked post</Trans> 594 - } else if (notFound) { 595 - label = <Trans context="description">Reply to a post</Trans> 596 - } else if (profile != null) { 597 - const isMe = profile.did === currentAccount?.did 598 - if (isMe) { 599 - label = <Trans context="description">Reply to you</Trans> 600 - } else { 601 - label = ( 602 - <Trans context="description"> 603 - Reply to{' '} 604 - <ProfileHoverCard did={profile.did}> 605 - <TextLinkOnWebOnly 606 - type="md" 607 - style={pal.textLight} 608 - lineHeight={1.2} 609 - numberOfLines={1} 610 - href={makeProfileLink(profile)} 611 - text={ 612 - <Text emoji type="md" style={pal.textLight} lineHeight={1.2}> 613 - {profile.displayName 614 - ? sanitizeDisplayName(profile.displayName) 615 - : sanitizeHandle(profile.handle)} 616 - </Text> 617 - } 618 - /> 619 - </ProfileHoverCard> 620 - </Trans> 621 - ) 622 - } 623 - } 624 - 625 - if (!label) { 626 - // Should not happen. 627 - return null 628 - } 629 - 630 - return ( 631 - <View style={[s.flexRow, s.mb2, s.alignCenter]}> 632 - <FontAwesomeIcon 633 - icon="reply" 634 - size={9} 635 - style={[{color: pal.colors.textLight} as FontAwesomeIconStyle, s.mr5]} 636 - /> 637 - <Text 638 - type="md" 639 - style={[pal.textLight, s.mr2]} 640 - lineHeight={1.2} 641 - numberOfLines={1}> 642 - {label} 643 - </Text> 644 - </View> 645 - ) 646 - } 647 - 648 575 const styles = StyleSheet.create({ 649 576 outer: { 650 577 paddingLeft: 10, 651 578 paddingRight: 15, 652 - // @ts-ignore web only -prf 653 579 cursor: 'pointer', 654 580 }, 655 581 replyLine: {
+29 -39
src/view/com/util/UserInfoText.tsx
··· 1 - import {StyleProp, StyleSheet, TextStyle} from 'react-native' 2 - import {AppBskyActorGetProfile as GetProfile} from '@atproto/api' 1 + import {type StyleProp, type TextStyle} from 'react-native' 2 + import {type AppBskyActorGetProfile} from '@atproto/api' 3 3 4 4 import {makeProfileLink} from '#/lib/routes/links' 5 5 import {sanitizeDisplayName} from '#/lib/strings/display-names' 6 6 import {sanitizeHandle} from '#/lib/strings/handles' 7 - import {TypographyVariant} from '#/lib/ThemeContext' 8 7 import {STALE} from '#/state/queries' 9 8 import {useProfileQuery} from '#/state/queries/profile' 10 - import {TextLinkOnWebOnly} from './Link' 9 + import {atoms as a} from '#/alf' 10 + import {InlineLinkText} from '#/components/Link' 11 + import {Text} from '#/components/Typography' 11 12 import {LoadingPlaceholder} from './LoadingPlaceholder' 12 - import {Text} from './text/Text' 13 13 14 14 export function UserInfoText({ 15 - type = 'md', 16 15 did, 17 16 attr, 18 17 failed, 19 18 prefix, 20 19 style, 21 20 }: { 22 - type?: TypographyVariant 23 21 did: string 24 - attr?: keyof GetProfile.OutputSchema 22 + attr?: keyof AppBskyActorGetProfile.OutputSchema 25 23 loading?: string 26 24 failed?: string 27 25 prefix?: string ··· 35 33 staleTime: STALE.INFINITY, 36 34 }) 37 35 38 - let inner 39 36 if (isError) { 40 - inner = ( 41 - <Text type={type} style={style} numberOfLines={1}> 37 + return ( 38 + <Text style={style} numberOfLines={1}> 42 39 {failed} 43 40 </Text> 44 41 ) 45 42 } else if (profile) { 46 - inner = ( 47 - <TextLinkOnWebOnly 48 - type={type} 43 + const text = `${prefix || ''}${sanitizeDisplayName( 44 + typeof profile[attr] === 'string' && profile[attr] 45 + ? (profile[attr] as string) 46 + : sanitizeHandle(profile.handle), 47 + )}` 48 + return ( 49 + <InlineLinkText 50 + label={text} 49 51 style={style} 50 - lineHeight={1.2} 51 52 numberOfLines={1} 52 - href={makeProfileLink(profile)} 53 - text={ 54 - <Text emoji type={type} style={style} lineHeight={1.2}> 55 - {`${prefix || ''}${sanitizeDisplayName( 56 - typeof profile[attr] === 'string' && profile[attr] 57 - ? (profile[attr] as string) 58 - : sanitizeHandle(profile.handle), 59 - )}`} 60 - </Text> 61 - } 62 - /> 63 - ) 64 - } else { 65 - inner = ( 66 - <LoadingPlaceholder 67 - width={80} 68 - height={8} 69 - style={styles.loadingPlaceholder} 70 - /> 53 + to={makeProfileLink(profile)}> 54 + <Text emoji style={style}> 55 + {text} 56 + </Text> 57 + </InlineLinkText> 71 58 ) 72 59 } 73 60 74 - return inner 61 + // eslint-disable-next-line bsky-internal/avoid-unwrapped-text 62 + return ( 63 + <LoadingPlaceholder 64 + width={80} 65 + height={8} 66 + style={[a.relative, {top: 1, left: 2}]} 67 + /> 68 + ) 75 69 } 76 - 77 - const styles = StyleSheet.create({ 78 - loadingPlaceholder: {position: 'relative', top: 1, left: 2}, 79 - })