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
zio/stable
Paul Frazee 2023-07-06 21:12:54 -05:00 committed by GitHub
parent df7552135a
commit 6f69157269
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 215 additions and 190 deletions

View File

@ -129,6 +129,7 @@ interface ScreenPropertiesMap {
Feed: {} Feed: {}
Notifications: {} Notifications: {}
Profile: {} Profile: {}
'Profile:Preview': {}
Settings: {} Settings: {}
AppPasswords: {} AppPasswords: {}
Moderation: {} Moderation: {}

View File

@ -31,6 +31,11 @@ export interface EditProfileModal {
onUpdate?: () => void onUpdate?: () => void
} }
export interface ProfilePreviewModal {
name: 'profile-preview'
did: string
}
export interface ServerInputModal { export interface ServerInputModal {
name: 'server-input' name: 'server-input'
initialService: string initialService: string
@ -128,6 +133,7 @@ export type Modal =
| ChangeHandleModal | ChangeHandleModal
| DeleteAccountModal | DeleteAccountModal
| EditProfileModal | EditProfileModal
| ProfilePreviewModal
// Curation // Curation
| ContentFilteringSettingsModal | ContentFilteringSettingsModal

View File

@ -9,6 +9,7 @@ import {usePalette} from 'lib/hooks/usePalette'
import * as ConfirmModal from './Confirm' import * as ConfirmModal from './Confirm'
import * as EditProfileModal from './EditProfile' import * as EditProfileModal from './EditProfile'
import * as ProfilePreviewModal from './ProfilePreview'
import * as ServerInputModal from './ServerInput' import * as ServerInputModal from './ServerInput'
import * as ReportPostModal from './report/ReportPost' import * as ReportPostModal from './report/ReportPost'
import * as RepostModal from './Repost' import * as RepostModal from './Repost'
@ -62,6 +63,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
} else if (activeModal?.name === 'edit-profile') { } else if (activeModal?.name === 'edit-profile') {
snapPoints = EditProfileModal.snapPoints snapPoints = EditProfileModal.snapPoints
element = <EditProfileModal.Component {...activeModal} /> element = <EditProfileModal.Component {...activeModal} />
} else if (activeModal?.name === 'profile-preview') {
snapPoints = ProfilePreviewModal.snapPoints
element = <ProfilePreviewModal.Component {...activeModal} />
} else if (activeModal?.name === 'server-input') { } else if (activeModal?.name === 'server-input') {
snapPoints = ServerInputModal.snapPoints snapPoints = ServerInputModal.snapPoints
element = <ServerInputModal.Component {...activeModal} /> element = <ServerInputModal.Component {...activeModal} />

View File

@ -8,6 +8,7 @@ import {isMobileWeb} from 'platform/detection'
import * as ConfirmModal from './Confirm' import * as ConfirmModal from './Confirm'
import * as EditProfileModal from './EditProfile' import * as EditProfileModal from './EditProfile'
import * as ProfilePreviewModal from './ProfilePreview'
import * as ServerInputModal from './ServerInput' import * as ServerInputModal from './ServerInput'
import * as ReportPostModal from './report/ReportPost' import * as ReportPostModal from './report/ReportPost'
import * as ReportAccountModal from './report/ReportAccount' import * as ReportAccountModal from './report/ReportAccount'
@ -68,6 +69,8 @@ function Modal({modal}: {modal: ModalIface}) {
element = <ConfirmModal.Component {...modal} /> element = <ConfirmModal.Component {...modal} />
} else if (modal.name === 'edit-profile') { } else if (modal.name === 'edit-profile') {
element = <EditProfileModal.Component {...modal} /> element = <EditProfileModal.Component {...modal} />
} else if (modal.name === 'profile-preview') {
element = <ProfilePreviewModal.Component {...modal} />
} else if (modal.name === 'server-input') { } else if (modal.name === 'server-input') {
element = <ServerInputModal.Component {...modal} /> element = <ServerInputModal.Component {...modal} />
} else if (modal.name === 'report-post') { } else if (modal.name === 'report-post') {

View File

@ -0,0 +1,89 @@
import React, {useState, useEffect, useCallback} from 'react'
import {StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {useNavigation, StackActions} from '@react-navigation/native'
import {Text} from '../util/text/Text'
import {useStores} from 'state/index'
import {ProfileModel} from 'state/models/content/profile'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics/analytics'
import {ProfileHeader} from '../profile/ProfileHeader'
import {Button} from '../util/forms/Button'
import {NavigationProp} from 'lib/routes/types'
export const snapPoints = [560]
export const Component = observer(({did}: {did: string}) => {
const store = useStores()
const pal = usePalette('default')
const palInverted = usePalette('inverted')
const navigation = useNavigation<NavigationProp>()
const [model] = useState(new ProfileModel(store, {actor: did}))
const {screen} = useAnalytics()
useEffect(() => {
screen('Profile:Preview')
model.setup()
}, [model, screen])
const onPressViewProfile = useCallback(() => {
navigation.dispatch(StackActions.push('Profile', {name: model.handle}))
store.shell.closeModal()
}, [navigation, store, model])
return (
<View style={pal.view}>
<View style={styles.headerWrapper}>
<ProfileHeader view={model} hideBackButton onRefreshAll={() => {}} />
</View>
<View style={[styles.buttonsContainer, pal.view]}>
<View style={styles.buttons}>
<Button
type="inverted"
style={[styles.button, styles.buttonWide]}
onPress={onPressViewProfile}
accessibilityLabel="View profile"
accessibilityHint="">
<Text type="button-lg" style={palInverted.text}>
View Profile
</Text>
</Button>
<Button
type="default"
style={styles.button}
onPress={() => store.shell.closeModal()}
accessibilityLabel="Close this preview"
accessibilityHint="">
<Text type="button-lg" style={pal.text}>
Close
</Text>
</Button>
</View>
</View>
</View>
)
})
const styles = StyleSheet.create({
headerWrapper: {
height: 440,
},
buttonsContainer: {
height: 120,
},
buttons: {
flexDirection: 'row',
gap: 8,
paddingHorizontal: 14,
paddingTop: 16,
},
button: {
flex: 2,
flexDirection: 'row',
justifyContent: 'center',
paddingVertical: 12,
},
buttonWide: {
flex: 3,
},
})

View File

@ -13,7 +13,7 @@ import {RichText} from '../util/text/RichText'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {PostDropdownBtn} from '../util/forms/DropdownButton' import {PostDropdownBtn} from '../util/forms/DropdownButton'
import * as Toast from '../util/Toast' import * as Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar' import {PreviewableUserAvatar} from '../util/UserAvatar'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {ago, niceDate} from 'lib/strings/time' import {ago, niceDate} from 'lib/strings/time'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
@ -163,22 +163,17 @@ export const PostThreadItem = observer(function PostThreadItem({
<PostSandboxWarning /> <PostSandboxWarning />
<View style={styles.layout}> <View style={styles.layout}>
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
<Link <PreviewableUserAvatar
href={authorHref} size={52}
title={authorTitle} did={item.post.author.did}
asAnchor handle={item.post.author.handle}
accessibilityLabel={`${item.post.author.handle}'s avatar`} avatar={item.post.author.avatar}
accessibilityHint=""> moderation={item.moderation.avatar}
<UserAvatar />
size={52}
avatar={item.post.author.avatar}
moderation={item.moderation.avatar}
/>
</Link>
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
<View style={[styles.meta, styles.metaExpandedLine1]}> <View style={[styles.meta, styles.metaExpandedLine1]}>
<View style={[s.flexRow, s.alignBaseline]}> <View style={[s.flexRow]}>
<Link <Link
style={styles.metaItem} style={styles.metaItem}
href={authorHref} href={authorHref}
@ -353,13 +348,13 @@ export const PostThreadItem = observer(function PostThreadItem({
<PostSandboxWarning /> <PostSandboxWarning />
<View style={styles.layout}> <View style={styles.layout}>
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
<Link href={authorHref} title={authorTitle} asAnchor> <PreviewableUserAvatar
<UserAvatar size={52}
size={52} did={item.post.author.did}
avatar={item.post.author.avatar} handle={item.post.author.handle}
moderation={item.moderation.avatar} avatar={item.post.author.avatar}
/> moderation={item.moderation.avatar}
</Link> />
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
<PostMeta <PostMeta
@ -368,7 +363,6 @@ export const PostThreadItem = observer(function PostThreadItem({
authorHasWarning={!!item.post.author.labels?.length} authorHasWarning={!!item.post.author.labels?.length}
timestamp={item.post.indexedAt} timestamp={item.post.indexedAt}
postHref={itemHref} postHref={itemHref}
did={item.post.author.did}
/> />
<ContentHider <ContentHider
moderation={item.moderation.thread} moderation={item.moderation.thread}

View File

@ -229,7 +229,6 @@ const PostLoaded = observer(
authorHasWarning={!!item.post.author.labels?.length} authorHasWarning={!!item.post.author.labels?.length}
timestamp={item.post.indexedAt} timestamp={item.post.indexedAt}
postHref={itemHref} postHref={itemHref}
did={item.post.author.did}
/> />
{replyAuthorDid !== '' && ( {replyAuthorDid !== '' && (
<View style={[s.flexRow, s.mb2, s.alignCenter]}> <View style={[s.flexRow, s.mb2, s.alignCenter]}>

View File

@ -28,7 +28,6 @@ const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
export const Feed = observer(function Feed({ export const Feed = observer(function Feed({
feed, feed,
style, style,
showPostFollowBtn,
scrollElRef, scrollElRef,
onPressTryAgain, onPressTryAgain,
onScroll, onScroll,
@ -41,7 +40,6 @@ export const Feed = observer(function Feed({
}: { }: {
feed: PostsFeedModel feed: PostsFeedModel
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
showPostFollowBtn?: boolean
scrollElRef?: MutableRefObject<FlatList<any> | null> scrollElRef?: MutableRefObject<FlatList<any> | null>
onPressTryAgain?: () => void onPressTryAgain?: () => void
onScroll?: OnScrollCb onScroll?: OnScrollCb
@ -138,15 +136,9 @@ export const Feed = observer(function Feed({
} else if (item === LOADING_ITEM) { } else if (item === LOADING_ITEM) {
return <PostFeedLoadingPlaceholder /> return <PostFeedLoadingPlaceholder />
} }
return <FeedSlice slice={item} showFollowBtn={showPostFollowBtn} /> return <FeedSlice slice={item} />
}, },
[ [feed, onPressTryAgain, onPressRetryLoadMore, renderEmptyState],
feed,
onPressTryAgain,
onPressRetryLoadMore,
showPostFollowBtn,
renderEmptyState,
],
) )
const FeedFooter = React.useCallback( const FeedFooter = React.useCallback(

View File

@ -21,7 +21,7 @@ import {ImageHider} from '../util/moderation/ImageHider'
import {RichText} from '../util/text/RichText' import {RichText} from '../util/text/RichText'
import {PostSandboxWarning} from '../util/PostSandboxWarning' import {PostSandboxWarning} from '../util/PostSandboxWarning'
import * as Toast from '../util/Toast' import * as Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar' import {PreviewableUserAvatar} from '../util/UserAvatar'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
@ -33,14 +33,12 @@ export const FeedItem = observer(function ({
item, item,
isThreadChild, isThreadChild,
isThreadParent, isThreadParent,
showFollowBtn,
ignoreMuteFor, ignoreMuteFor,
}: { }: {
item: PostsFeedItemModel item: PostsFeedItemModel
isThreadChild?: boolean isThreadChild?: boolean
isThreadParent?: boolean isThreadParent?: boolean
showReplyLine?: boolean showReplyLine?: boolean
showFollowBtn?: boolean
ignoreMuteFor?: string ignoreMuteFor?: string
}) { }) {
const store = useStores() const store = useStores()
@ -55,7 +53,6 @@ export const FeedItem = observer(function ({
return `/profile/${item.post.author.handle}/post/${urip.rkey}` return `/profile/${item.post.author.handle}/post/${urip.rkey}`
}, [item.post.uri, item.post.author.handle]) }, [item.post.uri, item.post.author.handle])
const itemTitle = `Post by ${item.post.author.handle}` const itemTitle = `Post by ${item.post.author.handle}`
const authorHref = `/profile/${item.post.author.handle}`
const replyAuthorDid = useMemo(() => { const replyAuthorDid = useMemo(() => {
if (!record?.reply) { if (!record?.reply) {
return '' return ''
@ -214,13 +211,13 @@ export const FeedItem = observer(function ({
<PostSandboxWarning /> <PostSandboxWarning />
<View style={styles.layout}> <View style={styles.layout}>
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
<Link href={authorHref} title={item.post.author.handle} asAnchor> <PreviewableUserAvatar
<UserAvatar size={52}
size={52} did={item.post.author.did}
avatar={item.post.author.avatar} handle={item.post.author.handle}
moderation={item.moderation.avatar} avatar={item.post.author.avatar}
/> moderation={item.moderation.avatar}
</Link> />
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
<PostMeta <PostMeta
@ -229,8 +226,6 @@ export const FeedItem = observer(function ({
authorHasWarning={!!item.post.author.labels?.length} authorHasWarning={!!item.post.author.labels?.length}
timestamp={item.post.indexedAt} timestamp={item.post.indexedAt}
postHref={itemHref} postHref={itemHref}
did={item.post.author.did}
showFollowBtn={showFollowBtn}
/> />
{!isThreadChild && replyAuthorDid !== '' && ( {!isThreadChild && replyAuthorDid !== '' && (
<View style={[s.flexRow, s.mb2, s.alignCenter]}> <View style={[s.flexRow, s.mb2, s.alignCenter]}>
@ -357,9 +352,9 @@ const styles = StyleSheet.create({
layout: { layout: {
flexDirection: 'row', flexDirection: 'row',
marginTop: 1, marginTop: 1,
gap: 10,
}, },
layoutAvi: { layoutAvi: {
width: 70,
paddingLeft: 8, paddingLeft: 8,
}, },
layoutContent: { layoutContent: {

View File

@ -11,11 +11,9 @@ import {ModerationBehaviorCode} from 'lib/labeling/types'
export function FeedSlice({ export function FeedSlice({
slice, slice,
showFollowBtn,
ignoreMuteFor, ignoreMuteFor,
}: { }: {
slice: PostsFeedSliceModel slice: PostsFeedSliceModel
showFollowBtn?: boolean
ignoreMuteFor?: string ignoreMuteFor?: string
}) { }) {
if (slice.moderation.list.behavior === ModerationBehaviorCode.Hide) { if (slice.moderation.list.behavior === ModerationBehaviorCode.Hide) {
@ -32,7 +30,6 @@ export function FeedSlice({
item={slice.items[0]} item={slice.items[0]}
isThreadParent={slice.isThreadParentAt(0)} isThreadParent={slice.isThreadParentAt(0)}
isThreadChild={slice.isThreadChildAt(0)} isThreadChild={slice.isThreadChildAt(0)}
showFollowBtn={showFollowBtn}
ignoreMuteFor={ignoreMuteFor} ignoreMuteFor={ignoreMuteFor}
/> />
<FeedItem <FeedItem
@ -40,7 +37,6 @@ export function FeedSlice({
item={slice.items[1]} item={slice.items[1]}
isThreadParent={slice.isThreadParentAt(1)} isThreadParent={slice.isThreadParentAt(1)}
isThreadChild={slice.isThreadChildAt(1)} isThreadChild={slice.isThreadChildAt(1)}
showFollowBtn={showFollowBtn}
ignoreMuteFor={ignoreMuteFor} ignoreMuteFor={ignoreMuteFor}
/> />
<ViewFullThread slice={slice} /> <ViewFullThread slice={slice} />
@ -49,7 +45,6 @@ export function FeedSlice({
item={slice.items[last]} item={slice.items[last]}
isThreadParent={slice.isThreadParentAt(last)} isThreadParent={slice.isThreadParentAt(last)}
isThreadChild={slice.isThreadChildAt(last)} isThreadChild={slice.isThreadChildAt(last)}
showFollowBtn={showFollowBtn}
ignoreMuteFor={ignoreMuteFor} ignoreMuteFor={ignoreMuteFor}
/> />
</> </>
@ -64,7 +59,6 @@ export function FeedSlice({
item={item} item={item}
isThreadParent={slice.isThreadParentAt(i)} isThreadParent={slice.isThreadParentAt(i)}
isThreadChild={slice.isThreadChildAt(i)} isThreadChild={slice.isThreadChildAt(i)}
showFollowBtn={showFollowBtn}
ignoreMuteFor={ignoreMuteFor} ignoreMuteFor={ignoreMuteFor}
/> />
))} ))}

View File

@ -28,7 +28,6 @@ import {CogIcon} from 'lib/icons'
export const MultiFeed = observer(function Feed({ export const MultiFeed = observer(function Feed({
multifeed, multifeed,
style, style,
showPostFollowBtn,
scrollElRef, scrollElRef,
onScroll, onScroll,
scrollEventThrottle, scrollEventThrottle,
@ -38,7 +37,6 @@ export const MultiFeed = observer(function Feed({
}: { }: {
multifeed: PostsMultiFeedModel multifeed: PostsMultiFeedModel
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
showPostFollowBtn?: boolean
scrollElRef?: MutableRefObject<FlatList<any> | null> scrollElRef?: MutableRefObject<FlatList<any> | null>
onPressTryAgain?: () => void onPressTryAgain?: () => void
onScroll?: OnScrollCb onScroll?: OnScrollCb
@ -105,9 +103,7 @@ export const MultiFeed = observer(function Feed({
</View> </View>
) )
} else if (item.type === 'feed-slice') { } else if (item.type === 'feed-slice') {
return ( return <FeedSlice slice={item.slice} />
<FeedSlice slice={item.slice} showFollowBtn={showPostFollowBtn} />
)
} else if (item.type === 'feed-loading') { } else if (item.type === 'feed-loading') {
return <PostFeedLoadingPlaceholder /> return <PostFeedLoadingPlaceholder />
} else if (item.type === 'feed-error') { } else if (item.type === 'feed-error') {
@ -139,7 +135,7 @@ export const MultiFeed = observer(function Feed({
} }
return null return null
}, },
[showPostFollowBtn, pal], [pal],
) )
const ListFooter = React.useCallback( const ListFooter = React.useCallback(

View File

@ -6,10 +6,7 @@ import {
TouchableWithoutFeedback, TouchableWithoutFeedback,
View, View,
} from 'react-native' } from 'react-native'
import { import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {BlurView} from '../util/BlurView' import {BlurView} from '../util/BlurView'
import {ProfileModel} from 'state/models/content/profile' import {ProfileModel} from 'state/models/content/profile'
@ -102,6 +99,7 @@ export const ProfileHeader = observer(
const ProfileHeaderLoaded = observer( const ProfileHeaderLoaded = observer(
({view, onRefreshAll, hideBackButton = false}: Props) => { ({view, onRefreshAll, hideBackButton = false}: Props) => {
const pal = usePalette('default') const pal = usePalette('default')
const palInverted = usePalette('inverted')
const store = useStores() const store = useStores()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {track} = useAnalytics() const {track} = useAnalytics()
@ -351,15 +349,15 @@ const ProfileHeaderLoaded = observer(
<TouchableOpacity <TouchableOpacity
testID="followBtn" testID="followBtn"
onPress={onPressToggleFollow} onPress={onPressToggleFollow}
style={[styles.btn, styles.primaryBtn]} style={[styles.btn, styles.mainBtn, palInverted.view]}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={`Follow ${view.handle}`} accessibilityLabel={`Follow ${view.handle}`}
accessibilityHint={`Shows direct posts from ${view.handle} in your feed`}> accessibilityHint={`Shows direct posts from ${view.handle} in your feed`}>
<FontAwesomeIcon <FontAwesomeIcon
icon="plus" icon="plus"
style={[s.white as FontAwesomeIconStyle, s.mr5]} style={[palInverted.text, s.mr5]}
/> />
<Text type="button" style={[s.white, s.bold]}> <Text type="button" style={[palInverted.text, s.bold]}>
Follow Follow
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
@ -609,7 +607,6 @@ const styles = StyleSheet.create({
}, },
description: { description: {
flex: 1,
marginBottom: 8, marginBottom: 8,
}, },

View File

@ -6,6 +6,7 @@ import {
Platform, Platform,
StyleProp, StyleProp,
TextStyle, TextStyle,
TextProps,
View, View,
ViewStyle, ViewStyle,
TouchableOpacity, TouchableOpacity,
@ -144,7 +145,7 @@ export const TextLink = observer(function TextLink({
numberOfLines?: number numberOfLines?: number
lineHeight?: number lineHeight?: number
dataSet?: any dataSet?: any
}) { } & TextProps) {
const {...props} = useLinkProps({to: sanitizeUrl(href)}) const {...props} = useLinkProps({to: sanitizeUrl(href)})
const store = useStores() const store = useStores()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
@ -186,16 +187,7 @@ export const TextLink = observer(function TextLink({
/** /**
* Only acts as a link on desktop web * Only acts as a link on desktop web
*/ */
export const DesktopWebTextLink = observer(function DesktopWebTextLink({ interface DesktopWebTextLinkProps extends TextProps {
testID,
type = 'md',
style,
href,
text,
numberOfLines,
lineHeight,
...props
}: {
testID?: string testID?: string
type?: TypographyVariant type?: TypographyVariant
style?: StyleProp<TextStyle> style?: StyleProp<TextStyle>
@ -206,7 +198,17 @@ export const DesktopWebTextLink = observer(function DesktopWebTextLink({
accessible?: boolean accessible?: boolean
accessibilityLabel?: string accessibilityLabel?: string
accessibilityHint?: string accessibilityHint?: string
}) { }
export const DesktopWebTextLink = observer(function DesktopWebTextLink({
testID,
type = 'md',
style,
href,
text,
numberOfLines,
lineHeight,
...props
}: DesktopWebTextLinkProps) {
if (isDesktopWeb) { if (isDesktopWeb) {
return ( return (
<TextLink <TextLink

View File

@ -4,12 +4,10 @@ import {Text} from './text/Text'
import {DesktopWebTextLink} from './Link' import {DesktopWebTextLink} from './Link'
import {ago, niceDate} from 'lib/strings/time' import {ago, niceDate} from 'lib/strings/time'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index'
import {UserAvatar} from './UserAvatar' import {UserAvatar} from './UserAvatar'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {FollowButton} from '../profile/FollowButton'
import {FollowState} from 'state/models/cache/my-follows'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
import {isAndroid, isIOS} from 'platform/detection'
interface PostMetaOpts { interface PostMetaOpts {
authorAvatar?: string authorAvatar?: string
@ -18,88 +16,17 @@ interface PostMetaOpts {
authorHasWarning: boolean authorHasWarning: boolean
postHref: string postHref: string
timestamp: string timestamp: string
did?: string
showFollowBtn?: boolean
} }
export const PostMeta = observer(function (opts: PostMetaOpts) { export const PostMeta = observer(function (opts: PostMetaOpts) {
const pal = usePalette('default') const pal = usePalette('default')
const displayName = opts.authorDisplayName || opts.authorHandle const displayName = opts.authorDisplayName || opts.authorHandle
const handle = opts.authorHandle const handle = opts.authorHandle
const store = useStores()
const isMe = opts.did === store.me.did
const followState =
typeof opts.did === 'string'
? store.me.follows.getFollowState(opts.did)
: FollowState.Unknown
const [didFollow, setDidFollow] = React.useState(false)
const onToggleFollow = React.useCallback(() => {
setDidFollow(true)
}, [setDidFollow])
if (
opts.showFollowBtn &&
!isMe &&
(followState === FollowState.NotFollowing || didFollow) &&
opts.did
) {
// two-liner with follow button
return (
<View style={styles.metaTwoLine}>
<View style={styles.metaTwoLineLeft}>
<View style={styles.metaTwoLineTop}>
<DesktopWebTextLink
type="lg-bold"
style={pal.text}
numberOfLines={1}
lineHeight={1.2}
text={sanitizeDisplayName(displayName)}
href={`/profile/${opts.authorHandle}`}
/>
<Text
type="md"
style={pal.textLight}
lineHeight={1.2}
accessible={false}>
&nbsp;&middot;&nbsp;
</Text>
<DesktopWebTextLink
type="md"
style={[styles.metaItem, pal.textLight]}
lineHeight={1.2}
text={ago(opts.timestamp)}
accessibilityLabel={niceDate(opts.timestamp)}
accessibilityHint=""
href={opts.postHref}
/>
</View>
<DesktopWebTextLink
type="md"
style={[styles.metaItem, pal.textLight]}
lineHeight={1.2}
numberOfLines={1}
text={`@${handle}`}
href={`/profile/${opts.authorHandle}`}
/>
</View>
<View>
<FollowButton
unfollowedType="default"
did={opts.did}
onToggleFollow={onToggleFollow}
/>
</View>
</View>
)
}
// one-liner
return ( return (
<View style={styles.meta}> <View style={styles.metaOneLine}>
{typeof opts.authorAvatar !== 'undefined' && ( {typeof opts.authorAvatar !== 'undefined' && (
<View style={[styles.metaItem, styles.avatar]}> <View style={styles.avatar}>
<UserAvatar <UserAvatar
avatar={opts.authorAvatar} avatar={opts.authorAvatar}
size={16} size={16}
@ -107,7 +34,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
/> />
</View> </View>
)} )}
<View style={[styles.metaItem, styles.maxWidth]}> <View style={styles.maxWidth}>
<DesktopWebTextLink <DesktopWebTextLink
type="lg-bold" type="lg-bold"
style={pal.text} style={pal.text}
@ -128,12 +55,18 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
href={`/profile/${opts.authorHandle}`} href={`/profile/${opts.authorHandle}`}
/> />
</View> </View>
<Text type="md" style={pal.textLight} lineHeight={1.2} accessible={false}> {!isAndroid && (
&middot;&nbsp; <Text
</Text> type="md"
style={pal.textLight}
lineHeight={1.2}
accessible={false}>
&middot;
</Text>
)}
<DesktopWebTextLink <DesktopWebTextLink
type="md" type="md"
style={[styles.metaItem, pal.textLight]} style={pal.textLight}
lineHeight={1.2} lineHeight={1.2}
text={ago(opts.timestamp)} text={ago(opts.timestamp)}
accessibilityLabel={niceDate(opts.timestamp)} accessibilityLabel={niceDate(opts.timestamp)}
@ -145,32 +78,16 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
}) })
const styles = StyleSheet.create({ const styles = StyleSheet.create({
meta: { metaOneLine: {
flexDirection: 'row', flexDirection: 'row',
paddingBottom: 2, paddingBottom: 2,
}, gap: 4,
metaTwoLine: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
paddingBottom: 4,
},
metaTwoLineLeft: {
flex: 1,
paddingRight: 40,
},
metaTwoLineTop: {
flexDirection: 'row',
alignItems: 'baseline',
},
metaItem: {
paddingRight: 5,
}, },
avatar: { avatar: {
alignSelf: 'center', alignSelf: 'center',
}, },
maxWidth: { maxWidth: {
maxWidth: '80%', flex: isAndroid ? 1 : undefined,
maxWidth: isIOS ? '80%' : undefined,
}, },
}) })

View File

@ -1,5 +1,5 @@
import React, {useMemo} from 'react' import React, {useMemo} from 'react'
import {StyleSheet, View} from 'react-native' import {Pressable, StyleSheet, View} from 'react-native'
import Svg, {Circle, Rect, Path} from 'react-native-svg' import Svg, {Circle, Rect, Path} from 'react-native-svg'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {IconProp} from '@fortawesome/fontawesome-svg-core' import {IconProp} from '@fortawesome/fontawesome-svg-core'
@ -12,13 +12,31 @@ import {
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {colors} from 'lib/styles' import {colors} from 'lib/styles'
import {DropdownButton} from './forms/DropdownButton' import {DropdownButton} from './forms/DropdownButton'
import {Link} from './Link'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {isWeb, isAndroid} from 'platform/detection' import {isWeb, isAndroid} from 'platform/detection'
import {Image as RNImage} from 'react-native-image-crop-picker' import {Image as RNImage} from 'react-native-image-crop-picker'
import {AvatarModeration} from 'lib/labeling/types' import {AvatarModeration} from 'lib/labeling/types'
import {isDesktopWeb} from 'platform/detection'
type Type = 'user' | 'algo' | 'list' type Type = 'user' | 'algo' | 'list'
interface BaseUserAvatarProps {
type?: Type
size: number
avatar?: string | null
moderation?: AvatarModeration
}
interface UserAvatarProps extends BaseUserAvatarProps {
onSelectNewAvatar?: (img: RNImage | null) => void
}
interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
did: string
handle: string
}
const BLUR_AMOUNT = isWeb ? 5 : 100 const BLUR_AMOUNT = isWeb ? 5 : 100
function DefaultAvatar({type, size}: {type: Type; size: number}) { function DefaultAvatar({type, size}: {type: Type; size: number}) {
@ -91,13 +109,7 @@ export function UserAvatar({
avatar, avatar,
moderation, moderation,
onSelectNewAvatar, onSelectNewAvatar,
}: { }: UserAvatarProps) {
type?: Type
size: number
avatar?: string | null
moderation?: AvatarModeration
onSelectNewAvatar?: (img: RNImage | null) => void
}) {
const store = useStores() const store = useStores()
const pal = usePalette('default') const pal = usePalette('default')
const {requestCameraAccessIfNeeded} = useCameraPermission() const {requestCameraAccessIfNeeded} = useCameraPermission()
@ -244,6 +256,32 @@ export function UserAvatar({
) )
} }
export function PreviewableUserAvatar(props: PreviewableUserAvatarProps) {
const store = useStores()
if (isDesktopWeb) {
return (
<Link href={`/profile/${props.handle}`} title={props.handle} asAnchor>
<UserAvatar {...props} />
</Link>
)
}
return (
<Pressable
onPress={() =>
store.shell.openModal({
name: 'profile-preview',
did: props.did,
})
}
accessibilityRole="button"
accessibilityLabel={props.handle}
accessibilityHint="">
<UserAvatar {...props} />
</Pressable>
)
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
editButtonContainer: { editButtonContainer: {
position: 'absolute', position: 'absolute',

View File

@ -106,7 +106,6 @@ export const FeedsScreen = withAuthRequired(
onScroll={onMainScroll} onScroll={onMainScroll}
scrollEventThrottle={100} scrollEventThrottle={100}
headerOffset={HEADER_OFFSET} headerOffset={HEADER_OFFSET}
showPostFollowBtn
/> />
<ViewHeader <ViewHeader
title="My Feeds" title="My Feeds"

View File

@ -266,7 +266,6 @@ const FeedPage = observer(
key="default" key="default"
feed={feed} feed={feed}
scrollElRef={scrollElRef} scrollElRef={scrollElRef}
showPostFollowBtn
onPressTryAgain={onPressTryAgain} onPressTryAgain={onPressTryAgain}
onScroll={onMainScroll} onScroll={onMainScroll}
scrollEventThrottle={100} scrollEventThrottle={100}