Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Post UI updates (Profile Preview on mobile) (#990)

* Update postmeta to put the timestamp on the right side on mobile

* Drop the two-line PostMeta mode

* Add ProfilePreview modal

* Tune PostMeta to give the best behavior possible for a given platform

* Remove old showFollowBtn attributes

* Fix style issue

* Switch the follow button in the profile header to use the inverted color for consistency with the rest of the app

* Fix lint

* Fix darkmode

* Tune the profile preview footer

* Better analytics choice

authored by

Paul Frazee and committed by
GitHub
6f691572 df755213

+215 -190
+1
src/lib/analytics/types.ts
··· 129 129 Feed: {} 130 130 Notifications: {} 131 131 Profile: {} 132 + 'Profile:Preview': {} 132 133 Settings: {} 133 134 AppPasswords: {} 134 135 Moderation: {}
+6
src/state/models/ui/shell.ts
··· 31 31 onUpdate?: () => void 32 32 } 33 33 34 + export interface ProfilePreviewModal { 35 + name: 'profile-preview' 36 + did: string 37 + } 38 + 34 39 export interface ServerInputModal { 35 40 name: 'server-input' 36 41 initialService: string ··· 128 133 | ChangeHandleModal 129 134 | DeleteAccountModal 130 135 | EditProfileModal 136 + | ProfilePreviewModal 131 137 132 138 // Curation 133 139 | ContentFilteringSettingsModal
+4
src/view/com/modals/Modal.tsx
··· 9 9 10 10 import * as ConfirmModal from './Confirm' 11 11 import * as EditProfileModal from './EditProfile' 12 + import * as ProfilePreviewModal from './ProfilePreview' 12 13 import * as ServerInputModal from './ServerInput' 13 14 import * as ReportPostModal from './report/ReportPost' 14 15 import * as RepostModal from './Repost' ··· 62 63 } else if (activeModal?.name === 'edit-profile') { 63 64 snapPoints = EditProfileModal.snapPoints 64 65 element = <EditProfileModal.Component {...activeModal} /> 66 + } else if (activeModal?.name === 'profile-preview') { 67 + snapPoints = ProfilePreviewModal.snapPoints 68 + element = <ProfilePreviewModal.Component {...activeModal} /> 65 69 } else if (activeModal?.name === 'server-input') { 66 70 snapPoints = ServerInputModal.snapPoints 67 71 element = <ServerInputModal.Component {...activeModal} />
+3
src/view/com/modals/Modal.web.tsx
··· 8 8 9 9 import * as ConfirmModal from './Confirm' 10 10 import * as EditProfileModal from './EditProfile' 11 + import * as ProfilePreviewModal from './ProfilePreview' 11 12 import * as ServerInputModal from './ServerInput' 12 13 import * as ReportPostModal from './report/ReportPost' 13 14 import * as ReportAccountModal from './report/ReportAccount' ··· 68 69 element = <ConfirmModal.Component {...modal} /> 69 70 } else if (modal.name === 'edit-profile') { 70 71 element = <EditProfileModal.Component {...modal} /> 72 + } else if (modal.name === 'profile-preview') { 73 + element = <ProfilePreviewModal.Component {...modal} /> 71 74 } else if (modal.name === 'server-input') { 72 75 element = <ServerInputModal.Component {...modal} /> 73 76 } else if (modal.name === 'report-post') {
+89
src/view/com/modals/ProfilePreview.tsx
··· 1 + import React, {useState, useEffect, useCallback} from 'react' 2 + import {StyleSheet, View} from 'react-native' 3 + import {observer} from 'mobx-react-lite' 4 + import {useNavigation, StackActions} from '@react-navigation/native' 5 + import {Text} from '../util/text/Text' 6 + import {useStores} from 'state/index' 7 + import {ProfileModel} from 'state/models/content/profile' 8 + import {usePalette} from 'lib/hooks/usePalette' 9 + import {useAnalytics} from 'lib/analytics/analytics' 10 + import {ProfileHeader} from '../profile/ProfileHeader' 11 + import {Button} from '../util/forms/Button' 12 + import {NavigationProp} from 'lib/routes/types' 13 + 14 + export const snapPoints = [560] 15 + 16 + export const Component = observer(({did}: {did: string}) => { 17 + const store = useStores() 18 + const pal = usePalette('default') 19 + const palInverted = usePalette('inverted') 20 + const navigation = useNavigation<NavigationProp>() 21 + const [model] = useState(new ProfileModel(store, {actor: did})) 22 + const {screen} = useAnalytics() 23 + 24 + useEffect(() => { 25 + screen('Profile:Preview') 26 + model.setup() 27 + }, [model, screen]) 28 + 29 + const onPressViewProfile = useCallback(() => { 30 + navigation.dispatch(StackActions.push('Profile', {name: model.handle})) 31 + store.shell.closeModal() 32 + }, [navigation, store, model]) 33 + 34 + return ( 35 + <View style={pal.view}> 36 + <View style={styles.headerWrapper}> 37 + <ProfileHeader view={model} hideBackButton onRefreshAll={() => {}} /> 38 + </View> 39 + <View style={[styles.buttonsContainer, pal.view]}> 40 + <View style={styles.buttons}> 41 + <Button 42 + type="inverted" 43 + style={[styles.button, styles.buttonWide]} 44 + onPress={onPressViewProfile} 45 + accessibilityLabel="View profile" 46 + accessibilityHint=""> 47 + <Text type="button-lg" style={palInverted.text}> 48 + View Profile 49 + </Text> 50 + </Button> 51 + <Button 52 + type="default" 53 + style={styles.button} 54 + onPress={() => store.shell.closeModal()} 55 + accessibilityLabel="Close this preview" 56 + accessibilityHint=""> 57 + <Text type="button-lg" style={pal.text}> 58 + Close 59 + </Text> 60 + </Button> 61 + </View> 62 + </View> 63 + </View> 64 + ) 65 + }) 66 + 67 + const styles = StyleSheet.create({ 68 + headerWrapper: { 69 + height: 440, 70 + }, 71 + buttonsContainer: { 72 + height: 120, 73 + }, 74 + buttons: { 75 + flexDirection: 'row', 76 + gap: 8, 77 + paddingHorizontal: 14, 78 + paddingTop: 16, 79 + }, 80 + button: { 81 + flex: 2, 82 + flexDirection: 'row', 83 + justifyContent: 'center', 84 + paddingVertical: 12, 85 + }, 86 + buttonWide: { 87 + flex: 3, 88 + }, 89 + })
+16 -22
src/view/com/post-thread/PostThreadItem.tsx
··· 13 13 import {Text} from '../util/text/Text' 14 14 import {PostDropdownBtn} from '../util/forms/DropdownButton' 15 15 import * as Toast from '../util/Toast' 16 - import {UserAvatar} from '../util/UserAvatar' 16 + import {PreviewableUserAvatar} from '../util/UserAvatar' 17 17 import {s} from 'lib/styles' 18 18 import {ago, niceDate} from 'lib/strings/time' 19 19 import {sanitizeDisplayName} from 'lib/strings/display-names' ··· 163 163 <PostSandboxWarning /> 164 164 <View style={styles.layout}> 165 165 <View style={styles.layoutAvi}> 166 - <Link 167 - href={authorHref} 168 - title={authorTitle} 169 - asAnchor 170 - accessibilityLabel={`${item.post.author.handle}'s avatar`} 171 - accessibilityHint=""> 172 - <UserAvatar 173 - size={52} 174 - avatar={item.post.author.avatar} 175 - moderation={item.moderation.avatar} 176 - /> 177 - </Link> 166 + <PreviewableUserAvatar 167 + size={52} 168 + did={item.post.author.did} 169 + handle={item.post.author.handle} 170 + avatar={item.post.author.avatar} 171 + moderation={item.moderation.avatar} 172 + /> 178 173 </View> 179 174 <View style={styles.layoutContent}> 180 175 <View style={[styles.meta, styles.metaExpandedLine1]}> 181 - <View style={[s.flexRow, s.alignBaseline]}> 176 + <View style={[s.flexRow]}> 182 177 <Link 183 178 style={styles.metaItem} 184 179 href={authorHref} ··· 353 348 <PostSandboxWarning /> 354 349 <View style={styles.layout}> 355 350 <View style={styles.layoutAvi}> 356 - <Link href={authorHref} title={authorTitle} asAnchor> 357 - <UserAvatar 358 - size={52} 359 - avatar={item.post.author.avatar} 360 - moderation={item.moderation.avatar} 361 - /> 362 - </Link> 351 + <PreviewableUserAvatar 352 + size={52} 353 + did={item.post.author.did} 354 + handle={item.post.author.handle} 355 + avatar={item.post.author.avatar} 356 + moderation={item.moderation.avatar} 357 + /> 363 358 </View> 364 359 <View style={styles.layoutContent}> 365 360 <PostMeta ··· 368 363 authorHasWarning={!!item.post.author.labels?.length} 369 364 timestamp={item.post.indexedAt} 370 365 postHref={itemHref} 371 - did={item.post.author.did} 372 366 /> 373 367 <ContentHider 374 368 moderation={item.moderation.thread}
-1
src/view/com/post/Post.tsx
··· 229 229 authorHasWarning={!!item.post.author.labels?.length} 230 230 timestamp={item.post.indexedAt} 231 231 postHref={itemHref} 232 - did={item.post.author.did} 233 232 /> 234 233 {replyAuthorDid !== '' && ( 235 234 <View style={[s.flexRow, s.mb2, s.alignCenter]}>
+2 -10
src/view/com/posts/Feed.tsx
··· 28 28 export const Feed = observer(function Feed({ 29 29 feed, 30 30 style, 31 - showPostFollowBtn, 32 31 scrollElRef, 33 32 onPressTryAgain, 34 33 onScroll, ··· 41 40 }: { 42 41 feed: PostsFeedModel 43 42 style?: StyleProp<ViewStyle> 44 - showPostFollowBtn?: boolean 45 43 scrollElRef?: MutableRefObject<FlatList<any> | null> 46 44 onPressTryAgain?: () => void 47 45 onScroll?: OnScrollCb ··· 138 136 } else if (item === LOADING_ITEM) { 139 137 return <PostFeedLoadingPlaceholder /> 140 138 } 141 - return <FeedSlice slice={item} showFollowBtn={showPostFollowBtn} /> 139 + return <FeedSlice slice={item} /> 142 140 }, 143 - [ 144 - feed, 145 - onPressTryAgain, 146 - onPressRetryLoadMore, 147 - showPostFollowBtn, 148 - renderEmptyState, 149 - ], 141 + [feed, onPressTryAgain, onPressRetryLoadMore, renderEmptyState], 150 142 ) 151 143 152 144 const FeedFooter = React.useCallback(
+9 -14
src/view/com/posts/FeedItem.tsx
··· 21 21 import {RichText} from '../util/text/RichText' 22 22 import {PostSandboxWarning} from '../util/PostSandboxWarning' 23 23 import * as Toast from '../util/Toast' 24 - import {UserAvatar} from '../util/UserAvatar' 24 + import {PreviewableUserAvatar} from '../util/UserAvatar' 25 25 import {s} from 'lib/styles' 26 26 import {useStores} from 'state/index' 27 27 import {usePalette} from 'lib/hooks/usePalette' ··· 33 33 item, 34 34 isThreadChild, 35 35 isThreadParent, 36 - showFollowBtn, 37 36 ignoreMuteFor, 38 37 }: { 39 38 item: PostsFeedItemModel 40 39 isThreadChild?: boolean 41 40 isThreadParent?: boolean 42 41 showReplyLine?: boolean 43 - showFollowBtn?: boolean 44 42 ignoreMuteFor?: string 45 43 }) { 46 44 const store = useStores() ··· 55 53 return `/profile/${item.post.author.handle}/post/${urip.rkey}` 56 54 }, [item.post.uri, item.post.author.handle]) 57 55 const itemTitle = `Post by ${item.post.author.handle}` 58 - const authorHref = `/profile/${item.post.author.handle}` 59 56 const replyAuthorDid = useMemo(() => { 60 57 if (!record?.reply) { 61 58 return '' ··· 214 211 <PostSandboxWarning /> 215 212 <View style={styles.layout}> 216 213 <View style={styles.layoutAvi}> 217 - <Link href={authorHref} title={item.post.author.handle} asAnchor> 218 - <UserAvatar 219 - size={52} 220 - avatar={item.post.author.avatar} 221 - moderation={item.moderation.avatar} 222 - /> 223 - </Link> 214 + <PreviewableUserAvatar 215 + size={52} 216 + did={item.post.author.did} 217 + handle={item.post.author.handle} 218 + avatar={item.post.author.avatar} 219 + moderation={item.moderation.avatar} 220 + /> 224 221 </View> 225 222 <View style={styles.layoutContent}> 226 223 <PostMeta ··· 229 226 authorHasWarning={!!item.post.author.labels?.length} 230 227 timestamp={item.post.indexedAt} 231 228 postHref={itemHref} 232 - did={item.post.author.did} 233 - showFollowBtn={showFollowBtn} 234 229 /> 235 230 {!isThreadChild && replyAuthorDid !== '' && ( 236 231 <View style={[s.flexRow, s.mb2, s.alignCenter]}> ··· 357 352 layout: { 358 353 flexDirection: 'row', 359 354 marginTop: 1, 355 + gap: 10, 360 356 }, 361 357 layoutAvi: { 362 - width: 70, 363 358 paddingLeft: 8, 364 359 }, 365 360 layoutContent: {
-6
src/view/com/posts/FeedSlice.tsx
··· 11 11 12 12 export function FeedSlice({ 13 13 slice, 14 - showFollowBtn, 15 14 ignoreMuteFor, 16 15 }: { 17 16 slice: PostsFeedSliceModel 18 - showFollowBtn?: boolean 19 17 ignoreMuteFor?: string 20 18 }) { 21 19 if (slice.moderation.list.behavior === ModerationBehaviorCode.Hide) { ··· 32 30 item={slice.items[0]} 33 31 isThreadParent={slice.isThreadParentAt(0)} 34 32 isThreadChild={slice.isThreadChildAt(0)} 35 - showFollowBtn={showFollowBtn} 36 33 ignoreMuteFor={ignoreMuteFor} 37 34 /> 38 35 <FeedItem ··· 40 37 item={slice.items[1]} 41 38 isThreadParent={slice.isThreadParentAt(1)} 42 39 isThreadChild={slice.isThreadChildAt(1)} 43 - showFollowBtn={showFollowBtn} 44 40 ignoreMuteFor={ignoreMuteFor} 45 41 /> 46 42 <ViewFullThread slice={slice} /> ··· 49 45 item={slice.items[last]} 50 46 isThreadParent={slice.isThreadParentAt(last)} 51 47 isThreadChild={slice.isThreadChildAt(last)} 52 - showFollowBtn={showFollowBtn} 53 48 ignoreMuteFor={ignoreMuteFor} 54 49 /> 55 50 </> ··· 64 59 item={item} 65 60 isThreadParent={slice.isThreadParentAt(i)} 66 61 isThreadChild={slice.isThreadChildAt(i)} 67 - showFollowBtn={showFollowBtn} 68 62 ignoreMuteFor={ignoreMuteFor} 69 63 /> 70 64 ))}
+2 -6
src/view/com/posts/MultiFeed.tsx
··· 28 28 export const MultiFeed = observer(function Feed({ 29 29 multifeed, 30 30 style, 31 - showPostFollowBtn, 32 31 scrollElRef, 33 32 onScroll, 34 33 scrollEventThrottle, ··· 38 37 }: { 39 38 multifeed: PostsMultiFeedModel 40 39 style?: StyleProp<ViewStyle> 41 - showPostFollowBtn?: boolean 42 40 scrollElRef?: MutableRefObject<FlatList<any> | null> 43 41 onPressTryAgain?: () => void 44 42 onScroll?: OnScrollCb ··· 105 103 </View> 106 104 ) 107 105 } else if (item.type === 'feed-slice') { 108 - return ( 109 - <FeedSlice slice={item.slice} showFollowBtn={showPostFollowBtn} /> 110 - ) 106 + return <FeedSlice slice={item.slice} /> 111 107 } else if (item.type === 'feed-loading') { 112 108 return <PostFeedLoadingPlaceholder /> 113 109 } else if (item.type === 'feed-error') { ··· 139 135 } 140 136 return null 141 137 }, 142 - [showPostFollowBtn, pal], 138 + [pal], 143 139 ) 144 140 145 141 const ListFooter = React.useCallback(
+5 -8
src/view/com/profile/ProfileHeader.tsx
··· 6 6 TouchableWithoutFeedback, 7 7 View, 8 8 } from 'react-native' 9 - import { 10 - FontAwesomeIcon, 11 - FontAwesomeIconStyle, 12 - } from '@fortawesome/react-native-fontawesome' 9 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 13 10 import {useNavigation} from '@react-navigation/native' 14 11 import {BlurView} from '../util/BlurView' 15 12 import {ProfileModel} from 'state/models/content/profile' ··· 102 99 const ProfileHeaderLoaded = observer( 103 100 ({view, onRefreshAll, hideBackButton = false}: Props) => { 104 101 const pal = usePalette('default') 102 + const palInverted = usePalette('inverted') 105 103 const store = useStores() 106 104 const navigation = useNavigation<NavigationProp>() 107 105 const {track} = useAnalytics() ··· 351 349 <TouchableOpacity 352 350 testID="followBtn" 353 351 onPress={onPressToggleFollow} 354 - style={[styles.btn, styles.primaryBtn]} 352 + style={[styles.btn, styles.mainBtn, palInverted.view]} 355 353 accessibilityRole="button" 356 354 accessibilityLabel={`Follow ${view.handle}`} 357 355 accessibilityHint={`Shows direct posts from ${view.handle} in your feed`}> 358 356 <FontAwesomeIcon 359 357 icon="plus" 360 - style={[s.white as FontAwesomeIconStyle, s.mr5]} 358 + style={[palInverted.text, s.mr5]} 361 359 /> 362 - <Text type="button" style={[s.white, s.bold]}> 360 + <Text type="button" style={[palInverted.text, s.bold]}> 363 361 Follow 364 362 </Text> 365 363 </TouchableOpacity> ··· 609 607 }, 610 608 611 609 description: { 612 - flex: 1, 613 610 marginBottom: 8, 614 611 }, 615 612
+14 -12
src/view/com/util/Link.tsx
··· 6 6 Platform, 7 7 StyleProp, 8 8 TextStyle, 9 + TextProps, 9 10 View, 10 11 ViewStyle, 11 12 TouchableOpacity, ··· 144 145 numberOfLines?: number 145 146 lineHeight?: number 146 147 dataSet?: any 147 - }) { 148 + } & TextProps) { 148 149 const {...props} = useLinkProps({to: sanitizeUrl(href)}) 149 150 const store = useStores() 150 151 const navigation = useNavigation<NavigationProp>() ··· 186 187 /** 187 188 * Only acts as a link on desktop web 188 189 */ 189 - export const DesktopWebTextLink = observer(function DesktopWebTextLink({ 190 - testID, 191 - type = 'md', 192 - style, 193 - href, 194 - text, 195 - numberOfLines, 196 - lineHeight, 197 - ...props 198 - }: { 190 + interface DesktopWebTextLinkProps extends TextProps { 199 191 testID?: string 200 192 type?: TypographyVariant 201 193 style?: StyleProp<TextStyle> ··· 206 198 accessible?: boolean 207 199 accessibilityLabel?: string 208 200 accessibilityHint?: string 209 - }) { 201 + } 202 + export const DesktopWebTextLink = observer(function DesktopWebTextLink({ 203 + testID, 204 + type = 'md', 205 + style, 206 + href, 207 + text, 208 + numberOfLines, 209 + lineHeight, 210 + ...props 211 + }: DesktopWebTextLinkProps) { 210 212 if (isDesktopWeb) { 211 213 return ( 212 214 <TextLink
+18 -101
src/view/com/util/PostMeta.tsx
··· 4 4 import {DesktopWebTextLink} from './Link' 5 5 import {ago, niceDate} from 'lib/strings/time' 6 6 import {usePalette} from 'lib/hooks/usePalette' 7 - import {useStores} from 'state/index' 8 7 import {UserAvatar} from './UserAvatar' 9 8 import {observer} from 'mobx-react-lite' 10 - import {FollowButton} from '../profile/FollowButton' 11 - import {FollowState} from 'state/models/cache/my-follows' 12 9 import {sanitizeDisplayName} from 'lib/strings/display-names' 10 + import {isAndroid, isIOS} from 'platform/detection' 13 11 14 12 interface PostMetaOpts { 15 13 authorAvatar?: string ··· 18 16 authorHasWarning: boolean 19 17 postHref: string 20 18 timestamp: string 21 - did?: string 22 - showFollowBtn?: boolean 23 19 } 24 20 25 21 export const PostMeta = observer(function (opts: PostMetaOpts) { 26 22 const pal = usePalette('default') 27 23 const displayName = opts.authorDisplayName || opts.authorHandle 28 24 const handle = opts.authorHandle 29 - const store = useStores() 30 - const isMe = opts.did === store.me.did 31 - const followState = 32 - typeof opts.did === 'string' 33 - ? store.me.follows.getFollowState(opts.did) 34 - : FollowState.Unknown 35 25 36 - const [didFollow, setDidFollow] = React.useState(false) 37 - const onToggleFollow = React.useCallback(() => { 38 - setDidFollow(true) 39 - }, [setDidFollow]) 40 - 41 - if ( 42 - opts.showFollowBtn && 43 - !isMe && 44 - (followState === FollowState.NotFollowing || didFollow) && 45 - opts.did 46 - ) { 47 - // two-liner with follow button 48 - return ( 49 - <View style={styles.metaTwoLine}> 50 - <View style={styles.metaTwoLineLeft}> 51 - <View style={styles.metaTwoLineTop}> 52 - <DesktopWebTextLink 53 - type="lg-bold" 54 - style={pal.text} 55 - numberOfLines={1} 56 - lineHeight={1.2} 57 - text={sanitizeDisplayName(displayName)} 58 - href={`/profile/${opts.authorHandle}`} 59 - /> 60 - <Text 61 - type="md" 62 - style={pal.textLight} 63 - lineHeight={1.2} 64 - accessible={false}> 65 - &nbsp;&middot;&nbsp; 66 - </Text> 67 - <DesktopWebTextLink 68 - type="md" 69 - style={[styles.metaItem, pal.textLight]} 70 - lineHeight={1.2} 71 - text={ago(opts.timestamp)} 72 - accessibilityLabel={niceDate(opts.timestamp)} 73 - accessibilityHint="" 74 - href={opts.postHref} 75 - /> 76 - </View> 77 - <DesktopWebTextLink 78 - type="md" 79 - style={[styles.metaItem, pal.textLight]} 80 - lineHeight={1.2} 81 - numberOfLines={1} 82 - text={`@${handle}`} 83 - href={`/profile/${opts.authorHandle}`} 84 - /> 85 - </View> 86 - 87 - <View> 88 - <FollowButton 89 - unfollowedType="default" 90 - did={opts.did} 91 - onToggleFollow={onToggleFollow} 92 - /> 93 - </View> 94 - </View> 95 - ) 96 - } 97 - 98 - // one-liner 99 26 return ( 100 - <View style={styles.meta}> 27 + <View style={styles.metaOneLine}> 101 28 {typeof opts.authorAvatar !== 'undefined' && ( 102 - <View style={[styles.metaItem, styles.avatar]}> 29 + <View style={styles.avatar}> 103 30 <UserAvatar 104 31 avatar={opts.authorAvatar} 105 32 size={16} ··· 107 34 /> 108 35 </View> 109 36 )} 110 - <View style={[styles.metaItem, styles.maxWidth]}> 37 + <View style={styles.maxWidth}> 111 38 <DesktopWebTextLink 112 39 type="lg-bold" 113 40 style={pal.text} ··· 128 55 href={`/profile/${opts.authorHandle}`} 129 56 /> 130 57 </View> 131 - <Text type="md" style={pal.textLight} lineHeight={1.2} accessible={false}> 132 - &middot;&nbsp; 133 - </Text> 58 + {!isAndroid && ( 59 + <Text 60 + type="md" 61 + style={pal.textLight} 62 + lineHeight={1.2} 63 + accessible={false}> 64 + &middot; 65 + </Text> 66 + )} 134 67 <DesktopWebTextLink 135 68 type="md" 136 - style={[styles.metaItem, pal.textLight]} 69 + style={pal.textLight} 137 70 lineHeight={1.2} 138 71 text={ago(opts.timestamp)} 139 72 accessibilityLabel={niceDate(opts.timestamp)} ··· 145 78 }) 146 79 147 80 const styles = StyleSheet.create({ 148 - meta: { 81 + metaOneLine: { 149 82 flexDirection: 'row', 150 83 paddingBottom: 2, 151 - }, 152 - metaTwoLine: { 153 - flexDirection: 'row', 154 - alignItems: 'center', 155 - justifyContent: 'space-between', 156 - width: '100%', 157 - paddingBottom: 4, 158 - }, 159 - metaTwoLineLeft: { 160 - flex: 1, 161 - paddingRight: 40, 162 - }, 163 - metaTwoLineTop: { 164 - flexDirection: 'row', 165 - alignItems: 'baseline', 166 - }, 167 - metaItem: { 168 - paddingRight: 5, 84 + gap: 4, 169 85 }, 170 86 avatar: { 171 87 alignSelf: 'center', 172 88 }, 173 89 maxWidth: { 174 - maxWidth: '80%', 90 + flex: isAndroid ? 1 : undefined, 91 + maxWidth: isIOS ? '80%' : undefined, 175 92 }, 176 93 })
+46 -8
src/view/com/util/UserAvatar.tsx
··· 1 1 import React, {useMemo} from 'react' 2 - import {StyleSheet, View} from 'react-native' 2 + import {Pressable, StyleSheet, View} from 'react-native' 3 3 import Svg, {Circle, Rect, Path} from 'react-native-svg' 4 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 5 import {IconProp} from '@fortawesome/fontawesome-svg-core' ··· 12 12 import {useStores} from 'state/index' 13 13 import {colors} from 'lib/styles' 14 14 import {DropdownButton} from './forms/DropdownButton' 15 + import {Link} from './Link' 15 16 import {usePalette} from 'lib/hooks/usePalette' 16 17 import {isWeb, isAndroid} from 'platform/detection' 17 18 import {Image as RNImage} from 'react-native-image-crop-picker' 18 19 import {AvatarModeration} from 'lib/labeling/types' 20 + import {isDesktopWeb} from 'platform/detection' 19 21 20 22 type Type = 'user' | 'algo' | 'list' 23 + 24 + interface BaseUserAvatarProps { 25 + type?: Type 26 + size: number 27 + avatar?: string | null 28 + moderation?: AvatarModeration 29 + } 30 + 31 + interface UserAvatarProps extends BaseUserAvatarProps { 32 + onSelectNewAvatar?: (img: RNImage | null) => void 33 + } 34 + 35 + interface PreviewableUserAvatarProps extends BaseUserAvatarProps { 36 + did: string 37 + handle: string 38 + } 21 39 22 40 const BLUR_AMOUNT = isWeb ? 5 : 100 23 41 ··· 91 109 avatar, 92 110 moderation, 93 111 onSelectNewAvatar, 94 - }: { 95 - type?: Type 96 - size: number 97 - avatar?: string | null 98 - moderation?: AvatarModeration 99 - onSelectNewAvatar?: (img: RNImage | null) => void 100 - }) { 112 + }: UserAvatarProps) { 101 113 const store = useStores() 102 114 const pal = usePalette('default') 103 115 const {requestCameraAccessIfNeeded} = useCameraPermission() ··· 241 253 <DefaultAvatar type={type} size={size} /> 242 254 {warning} 243 255 </View> 256 + ) 257 + } 258 + 259 + export function PreviewableUserAvatar(props: PreviewableUserAvatarProps) { 260 + const store = useStores() 261 + 262 + if (isDesktopWeb) { 263 + return ( 264 + <Link href={`/profile/${props.handle}`} title={props.handle} asAnchor> 265 + <UserAvatar {...props} /> 266 + </Link> 267 + ) 268 + } 269 + return ( 270 + <Pressable 271 + onPress={() => 272 + store.shell.openModal({ 273 + name: 'profile-preview', 274 + did: props.did, 275 + }) 276 + } 277 + accessibilityRole="button" 278 + accessibilityLabel={props.handle} 279 + accessibilityHint=""> 280 + <UserAvatar {...props} /> 281 + </Pressable> 244 282 ) 245 283 } 246 284
-1
src/view/screens/Feeds.tsx
··· 106 106 onScroll={onMainScroll} 107 107 scrollEventThrottle={100} 108 108 headerOffset={HEADER_OFFSET} 109 - showPostFollowBtn 110 109 /> 111 110 <ViewHeader 112 111 title="My Feeds"
-1
src/view/screens/Home.tsx
··· 266 266 key="default" 267 267 feed={feed} 268 268 scrollElRef={scrollElRef} 269 - showPostFollowBtn 270 269 onPressTryAgain={onPressTryAgain} 271 270 onScroll={onMainScroll} 272 271 scrollEventThrottle={100}