Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[Neue] Post avi, `PostMeta` cleanup (#5450)

* Support emoji in text with custom font

* Add emoji support to elements that need it

* Remove unused file causing lint failure

* Add web only link variant

* Refactor PostMeta

* Reduce avi size in feeds

* Fix alignment, emoji, in PostMeta

* Smaller avis in notifications

* Shrink post placeholder avi

* Handle the handle again

* Link cleanup

* Cleanup unused props

* Fix text wrapping in timestamp

* Fix underline color

* Tighten up spacing

* Web only whiteSpace

authored by

Eric Bailey and committed by
GitHub
b38d4697 2429d5d1

+120 -117
+2 -2
src/App.web.tsx
··· 1 - import 'lib/sentry' // must be near top 2 - import 'view/icons' 1 + import '#/lib/sentry' // must be near top 2 + import '#/view/icons' 3 3 import './style.css' 4 4 5 5 import React, {useEffect, useState} from 'react'
+28 -8
src/components/Link.tsx
··· 9 9 import {StackActions, useLinkProps} from '@react-navigation/native' 10 10 11 11 import {BSKY_DOWNLOAD_URL} from '#/lib/constants' 12 + import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped' 12 13 import {AllNavigatorParams} from '#/lib/routes/types' 13 14 import {shareUrl} from '#/lib/sharing' 14 15 import { ··· 17 18 isExternalUrl, 18 19 linkRequiresWarning, 19 20 } from '#/lib/strings/url-helpers' 20 - import {isNative} from '#/platform/detection' 21 + import {isNative, isWeb} from '#/platform/detection' 21 22 import {shouldClickOpenNewTab} from '#/platform/urls' 22 23 import {useModalControls} from '#/state/modals' 23 24 import {useOpenLink} from '#/state/preferences/in-app-browser' 24 - import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped' 25 25 import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf' 26 26 import {Button, ButtonProps} from '#/components/Button' 27 27 import {useInteractionState} from '#/components/hooks/useInteractionState' ··· 244 244 export type InlineLinkProps = React.PropsWithChildren< 245 245 BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'> 246 246 > & 247 - Pick<ButtonProps, 'label'> 247 + Pick<ButtonProps, 'label'> & { 248 + disableUnderline?: boolean 249 + title?: TextProps['title'] 250 + } 248 251 249 252 export function InlineLinkText({ 250 253 children, ··· 257 260 selectable, 258 261 label, 259 262 shareOnLongPress, 263 + disableUnderline, 260 264 ...rest 261 265 }: InlineLinkProps) { 262 266 const t = useTheme() ··· 290 294 {...rest} 291 295 style={[ 292 296 {color: t.palette.primary_500}, 293 - (hovered || focused || pressed) && { 294 - ...web({outline: 0}), 295 - textDecorationLine: 'underline', 296 - textDecorationColor: flattenedStyle.color ?? t.palette.primary_500, 297 - }, 297 + (hovered || focused || pressed) && 298 + !disableUnderline && { 299 + ...web({outline: 0}), 300 + textDecorationLine: 'underline', 301 + textDecorationColor: flattenedStyle.color ?? t.palette.primary_500, 302 + }, 298 303 flattenedStyle, 299 304 ]} 300 305 role="link" ··· 365 370 </Pressable> 366 371 ) 367 372 } 373 + 374 + export function WebOnlyInlineLinkText({ 375 + children, 376 + to, 377 + onPress, 378 + ...props 379 + }: InlineLinkProps) { 380 + return isWeb ? ( 381 + <InlineLinkText {...props} to={to} onPress={onPress}> 382 + {children} 383 + </InlineLinkText> 384 + ) : ( 385 + <Text {...props}>{children}</Text> 386 + ) 387 + }
-1
src/screens/Messages/Conversation/MessageInputEmbed.tsx
··· 174 174 showAvatar 175 175 author={post.author} 176 176 moderation={moderation} 177 - authorHasWarning={!!post.author.labels?.length} 178 177 timestamp={post.indexedAt} 179 178 postHref={itemHref} 180 179 style={a.flex_0}
+2 -6
src/view/com/post-thread/PostThreadItem.tsx
··· 558 558 <PostMeta 559 559 author={post.author} 560 560 moderation={moderation} 561 - authorHasWarning={!!post.author.labels?.length} 562 561 timestamp={post.indexedAt} 563 562 postHref={postHref} 564 563 showAvatar={isThreadedChild} 565 564 avatarModeration={moderation.ui('avatar')} 566 - avatarSize={28} 567 - displayNameType="md-bold" 568 - displayNameStyle={isThreadedChild && s.ml2} 565 + avatarSize={24} 569 566 style={ 570 567 isThreadedChild && { 571 - alignItems: 'center', 572 - paddingBottom: isWeb ? 5 : 2, 568 + paddingBottom: isWeb ? 5 : 4, 573 569 } 574 570 } 575 571 />
+1 -2
src/view/com/post/Post.tsx
··· 163 163 <View style={styles.layoutAvi}> 164 164 <AviFollowButton author={post.author} moderation={moderation}> 165 165 <PreviewableUserAvatar 166 - size={52} 166 + size={42} 167 167 profile={post.author} 168 168 moderation={moderation.ui('avatar')} 169 169 type={post.author.associated?.labeler ? 'labeler' : 'user'} ··· 174 174 <PostMeta 175 175 author={post.author} 176 176 moderation={moderation} 177 - authorHasWarning={!!post.author.labels?.length} 178 177 timestamp={post.indexedAt} 179 178 postHref={itemHref} 180 179 />
+2 -3
src/view/com/posts/FeedItem.tsx
··· 245 245 onBeforePress={onBeforePress} 246 246 dataSet={{feedContext}}> 247 247 <View style={{flexDirection: 'row', gap: 10, paddingLeft: 8}}> 248 - <View style={{width: 52}}> 248 + <View style={{width: 42}}> 249 249 {isThreadChild && ( 250 250 <View 251 251 style={[ ··· 345 345 <View style={styles.layoutAvi}> 346 346 <AviFollowButton author={post.author} moderation={moderation}> 347 347 <PreviewableUserAvatar 348 - size={52} 348 + size={42} 349 349 profile={post.author} 350 350 moderation={moderation.ui('avatar')} 351 351 type={post.author.associated?.labeler ? 'labeler' : 'user'} ··· 369 369 <PostMeta 370 370 author={post.author} 371 371 moderation={moderation} 372 - authorHasWarning={!!post.author.labels?.length} 373 372 timestamp={post.indexedAt} 374 373 postHref={href} 375 374 onOpenAuthor={onOpenAuthor}
+3 -3
src/view/com/posts/FeedSlice.tsx
··· 4 4 import {AtUri} from '@atproto/api' 5 5 import {Trans} from '@lingui/macro' 6 6 7 + import {usePalette} from '#/lib/hooks/usePalette' 8 + import {makeProfileLink} from '#/lib/routes/links' 7 9 import {FeedPostSlice} from '#/state/queries/post-feed' 8 - import {usePalette} from 'lib/hooks/usePalette' 9 - import {makeProfileLink} from 'lib/routes/links' 10 10 import {Link} from '../util/Link' 11 11 import {Text} from '../util/text/Text' 12 12 import {FeedItem} from './FeedItem' ··· 146 146 paddingLeft: 18, 147 147 }, 148 148 viewFullThreadDots: { 149 - width: 52, 149 + width: 42, 150 150 alignItems: 'center', 151 151 }, 152 152 })
+5 -5
src/view/com/util/LoadingPlaceholder.tsx
··· 7 7 ViewStyle, 8 8 } from 'react-native' 9 9 10 - import {usePalette} from 'lib/hooks/usePalette' 11 - import {s} from 'lib/styles' 12 - import {useTheme} from 'lib/ThemeContext' 10 + import {usePalette} from '#/lib/hooks/usePalette' 11 + import {s} from '#/lib/styles' 12 + import {useTheme} from '#/lib/ThemeContext' 13 13 import {atoms as a, useTheme as useTheme_NEW} from '#/alf' 14 14 import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble' 15 15 import { ··· 53 53 return ( 54 54 <View style={[styles.post, pal.view, style]}> 55 55 <LoadingPlaceholder 56 - width={52} 57 - height={52} 56 + width={42} 57 + height={42} 58 58 style={[ 59 59 styles.avatar, 60 60 {
+72 -81
src/view/com/util/PostMeta.tsx
··· 1 1 import React, {memo, useCallback} from 'react' 2 - import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' 2 + import {StyleProp, View, ViewStyle} from 'react-native' 3 3 import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api' 4 + import {msg} from '@lingui/macro' 4 5 import {useLingui} from '@lingui/react' 5 6 import {useQueryClient} from '@tanstack/react-query' 6 7 7 - import {usePalette} from '#/lib/hooks/usePalette' 8 8 import {makeProfileLink} from '#/lib/routes/links' 9 9 import {forceLTR} from '#/lib/strings/bidi' 10 10 import {NON_BREAKING_SPACE} from '#/lib/strings/constants' 11 11 import {sanitizeDisplayName} from '#/lib/strings/display-names' 12 12 import {sanitizeHandle} from '#/lib/strings/handles' 13 13 import {niceDate} from '#/lib/strings/time' 14 - import {TypographyVariant} from '#/lib/ThemeContext' 15 - import {isAndroid} from '#/platform/detection' 16 14 import {precacheProfile} from '#/state/queries/profile' 15 + import {atoms as a, useTheme, web} from '#/alf' 16 + import {WebOnlyInlineLinkText} from '#/components/Link' 17 17 import {ProfileHoverCard} from '#/components/ProfileHoverCard' 18 - import {TextLinkOnWebOnly} from './Link' 19 - import {Text} from './text/Text' 18 + import {Text} from '#/components/Typography' 20 19 import {TimeElapsed} from './TimeElapsed' 21 20 import {PreviewableUserAvatar} from './UserAvatar' 22 21 23 22 interface PostMetaOpts { 24 23 author: AppBskyActorDefs.ProfileViewBasic 25 24 moderation: ModerationDecision | undefined 26 - authorHasWarning: boolean 27 25 postHref: string 28 26 timestamp: string 29 27 showAvatar?: boolean 30 28 avatarModeration?: ModerationUI 31 29 avatarSize?: number 32 - displayNameType?: TypographyVariant 33 - displayNameStyle?: StyleProp<TextStyle> 34 30 onOpenAuthor?: () => void 35 31 style?: StyleProp<ViewStyle> 36 32 } 37 33 38 34 let PostMeta = (opts: PostMetaOpts): React.ReactNode => { 39 - const {i18n} = useLingui() 35 + const t = useTheme() 36 + const {i18n, _} = useLingui() 40 37 41 - const pal = usePalette('default') 42 38 const displayName = opts.author.displayName || opts.author.handle 43 39 const handle = opts.author.handle 44 40 const profileLink = makeProfileLink(opts.author) ··· 53 49 }, [queryClient, opts.author]) 54 50 55 51 return ( 56 - <View style={[styles.container, opts.style]}> 52 + <View 53 + style={[ 54 + a.flex_1, 55 + a.flex_row, 56 + a.align_center, 57 + a.pb_2xs, 58 + a.gap_xs, 59 + a.z_10, 60 + opts.style, 61 + ]}> 57 62 {opts.showAvatar && ( 58 - <View style={styles.avatar}> 63 + <View style={[a.self_center, a.mr_2xs]}> 59 64 <PreviewableUserAvatar 60 65 size={opts.avatarSize || 16} 61 66 profile={opts.author} ··· 65 70 </View> 66 71 )} 67 72 <ProfileHoverCard inline did={opts.author.did}> 68 - <Text 69 - numberOfLines={1} 70 - style={[styles.maxWidth, pal.textLight, opts.displayNameStyle]}> 71 - <TextLinkOnWebOnly 72 - type={opts.displayNameType || 'lg-bold'} 73 - style={[pal.text]} 74 - lineHeight={1.2} 73 + <Text numberOfLines={1} style={[a.flex_shrink]}> 74 + <WebOnlyInlineLinkText 75 + to={profileLink} 76 + label={_(msg`View profile`)} 75 77 disableMismatchWarning 76 - text={ 77 - <Text 78 - type={opts.displayNameType || 'lg-bold'} 79 - emoji 80 - style={[pal.text]} 81 - lineHeight={1.2}> 82 - {forceLTR( 83 - sanitizeDisplayName( 84 - displayName, 85 - opts.moderation?.ui('displayName'), 86 - ), 87 - )} 88 - </Text> 89 - } 90 - href={profileLink} 91 - onBeforePress={onBeforePressAuthor} 92 - /> 93 - <TextLinkOnWebOnly 94 - type="md" 78 + onPress={onBeforePressAuthor} 79 + style={[t.atoms.text]}> 80 + <Text emoji style={[a.text_md, a.font_bold, a.leading_tight]}> 81 + {forceLTR( 82 + sanitizeDisplayName( 83 + displayName, 84 + opts.moderation?.ui('displayName'), 85 + ), 86 + )} 87 + </Text> 88 + </WebOnlyInlineLinkText> 89 + <WebOnlyInlineLinkText 90 + to={profileLink} 91 + label={_(msg`View profile`)} 95 92 disableMismatchWarning 96 - style={[pal.textLight, {flexShrink: 4}]} 97 - text={ 98 - <Text emoji style={[pal.textLight, {flexShrink: 4}]}> 99 - {NON_BREAKING_SPACE + sanitizeHandle(handle, '@')} 100 - </Text> 101 - } 102 - href={profileLink} 103 - onBeforePress={onBeforePressAuthor} 104 - anchorNoUnderline 105 - /> 93 + disableUnderline 94 + onPress={onBeforePressAuthor} 95 + style={[a.text_md, t.atoms.text_contrast_medium, a.leading_tight]}> 96 + <Text 97 + emoji 98 + style={[ 99 + a.text_md, 100 + t.atoms.text_contrast_medium, 101 + a.leading_tight, 102 + ]}> 103 + {NON_BREAKING_SPACE + sanitizeHandle(handle, '@')} 104 + </Text> 105 + </WebOnlyInlineLinkText> 106 106 </Text> 107 107 </ProfileHoverCard> 108 - {!isAndroid && ( 109 - <Text type="md" style={pal.textLight} accessible={false}> 110 - &middot; 111 - </Text> 112 - )} 108 + 109 + <Text 110 + style={[a.text_md, t.atoms.text_contrast_medium]} 111 + accessible={false}> 112 + &middot; 113 + </Text> 114 + 113 115 <TimeElapsed timestamp={opts.timestamp}> 114 116 {({timeElapsed}) => ( 115 - <TextLinkOnWebOnly 116 - type="md" 117 - style={pal.textLight} 118 - text={timeElapsed} 119 - accessibilityLabel={niceDate(i18n, opts.timestamp)} 117 + <WebOnlyInlineLinkText 118 + to={opts.postHref} 119 + label={niceDate(i18n, opts.timestamp)} 120 120 title={niceDate(i18n, opts.timestamp)} 121 - accessibilityHint="" 122 - href={opts.postHref} 123 - onBeforePress={onBeforePressPost} 124 - /> 121 + disableMismatchWarning 122 + disableUnderline 123 + onPress={onBeforePressPost} 124 + style={[ 125 + a.text_md, 126 + t.atoms.text_contrast_medium, 127 + a.leading_tight, 128 + web({ 129 + whiteSpace: 'nowrap', 130 + }), 131 + ]}> 132 + {timeElapsed} 133 + </WebOnlyInlineLinkText> 125 134 )} 126 135 </TimeElapsed> 127 136 </View> ··· 129 138 } 130 139 PostMeta = memo(PostMeta) 131 140 export {PostMeta} 132 - 133 - const styles = StyleSheet.create({ 134 - container: { 135 - flexDirection: 'row', 136 - alignItems: 'flex-end', 137 - paddingBottom: 2, 138 - gap: 4, 139 - zIndex: 1, 140 - flex: 1, 141 - }, 142 - avatar: { 143 - alignSelf: 'center', 144 - }, 145 - maxWidth: { 146 - flex: isAndroid ? 1 : undefined, 147 - flexShrink: isAndroid ? undefined : 1, 148 - }, 149 - })
+5 -6
src/view/com/util/post-embeds/QuoteEmbed.tsx
··· 24 24 import {useQueryClient} from '@tanstack/react-query' 25 25 26 26 import {HITSLOP_20} from '#/lib/constants' 27 + import {usePalette} from '#/lib/hooks/usePalette' 28 + import {InfoCircleIcon} from '#/lib/icons' 27 29 import {moderatePost_wrapped} from '#/lib/moderatePost_wrapped' 30 + import {makeProfileLink} from '#/lib/routes/links' 28 31 import {s} from '#/lib/styles' 29 32 import {useModerationOpts} from '#/state/preferences/moderation-opts' 33 + import {precacheProfile} from '#/state/queries/profile' 30 34 import {useSession} from '#/state/session' 31 - import {usePalette} from 'lib/hooks/usePalette' 32 - import {InfoCircleIcon} from 'lib/icons' 33 - import {makeProfileLink} from 'lib/routes/links' 34 - import {precacheProfile} from 'state/queries/profile' 35 - import {ComposerOptsQuote} from 'state/shell/composer' 35 + import {ComposerOptsQuote} from '#/state/shell/composer' 36 36 import {atoms as a, useTheme} from '#/alf' 37 37 import {RichText} from '#/components/RichText' 38 38 import {ContentHider} from '../../../../components/moderation/ContentHider' ··· 238 238 author={quote.author} 239 239 moderation={moderation} 240 240 showAvatar 241 - authorHasWarning={false} 242 241 postHref={itemHref} 243 242 timestamp={quote.indexedAt} 244 243 />