From 6f69157269b27c4bae9730334f93f295ef0d4b94 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 6 Jul 2023 21:12:54 -0500 Subject: 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 --- src/lib/analytics/types.ts | 1 + src/state/models/ui/shell.ts | 6 ++ src/view/com/modals/Modal.tsx | 4 + src/view/com/modals/Modal.web.tsx | 3 + src/view/com/modals/ProfilePreview.tsx | 89 +++++++++++++++++++++ src/view/com/post-thread/PostThreadItem.tsx | 38 ++++----- src/view/com/post/Post.tsx | 1 - src/view/com/posts/Feed.tsx | 12 +-- src/view/com/posts/FeedItem.tsx | 23 +++--- src/view/com/posts/FeedSlice.tsx | 6 -- src/view/com/posts/MultiFeed.tsx | 8 +- src/view/com/profile/ProfileHeader.tsx | 13 ++- src/view/com/util/Link.tsx | 26 +++--- src/view/com/util/PostMeta.tsx | 119 +++++----------------------- src/view/com/util/UserAvatar.tsx | 54 +++++++++++-- src/view/screens/Feeds.tsx | 1 - src/view/screens/Home.tsx | 1 - 17 files changed, 215 insertions(+), 190 deletions(-) create mode 100644 src/view/com/modals/ProfilePreview.tsx (limited to 'src') diff --git a/src/lib/analytics/types.ts b/src/lib/analytics/types.ts index 7caa9b357..585884632 100644 --- a/src/lib/analytics/types.ts +++ b/src/lib/analytics/types.ts @@ -129,6 +129,7 @@ interface ScreenPropertiesMap { Feed: {} Notifications: {} Profile: {} + 'Profile:Preview': {} Settings: {} AppPasswords: {} Moderation: {} diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index ba03fe1b5..c6e7289df 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -31,6 +31,11 @@ export interface EditProfileModal { onUpdate?: () => void } +export interface ProfilePreviewModal { + name: 'profile-preview' + did: string +} + export interface ServerInputModal { name: 'server-input' initialService: string @@ -128,6 +133,7 @@ export type Modal = | ChangeHandleModal | DeleteAccountModal | EditProfileModal + | ProfilePreviewModal // Curation | ContentFilteringSettingsModal diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index b276dabc0..ad8794e89 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -9,6 +9,7 @@ import {usePalette} from 'lib/hooks/usePalette' import * as ConfirmModal from './Confirm' import * as EditProfileModal from './EditProfile' +import * as ProfilePreviewModal from './ProfilePreview' import * as ServerInputModal from './ServerInput' import * as ReportPostModal from './report/ReportPost' import * as RepostModal from './Repost' @@ -62,6 +63,9 @@ export const ModalsContainer = observer(function ModalsContainer() { } else if (activeModal?.name === 'edit-profile') { snapPoints = EditProfileModal.snapPoints element = + } else if (activeModal?.name === 'profile-preview') { + snapPoints = ProfilePreviewModal.snapPoints + element = } else if (activeModal?.name === 'server-input') { snapPoints = ServerInputModal.snapPoints element = diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 77842d3e1..20312fe6b 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -8,6 +8,7 @@ import {isMobileWeb} from 'platform/detection' import * as ConfirmModal from './Confirm' import * as EditProfileModal from './EditProfile' +import * as ProfilePreviewModal from './ProfilePreview' import * as ServerInputModal from './ServerInput' import * as ReportPostModal from './report/ReportPost' import * as ReportAccountModal from './report/ReportAccount' @@ -68,6 +69,8 @@ function Modal({modal}: {modal: ModalIface}) { element = } else if (modal.name === 'edit-profile') { element = + } else if (modal.name === 'profile-preview') { + element = } else if (modal.name === 'server-input') { element = } else if (modal.name === 'report-post') { diff --git a/src/view/com/modals/ProfilePreview.tsx b/src/view/com/modals/ProfilePreview.tsx new file mode 100644 index 000000000..d3267644b --- /dev/null +++ b/src/view/com/modals/ProfilePreview.tsx @@ -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() + 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 ( + + + {}} /> + + + + + + + + + ) +}) + +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, + }, +}) diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index e1c73c0d5..133d38421 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -13,7 +13,7 @@ import {RichText} from '../util/text/RichText' import {Text} from '../util/text/Text' import {PostDropdownBtn} from '../util/forms/DropdownButton' import * as Toast from '../util/Toast' -import {UserAvatar} from '../util/UserAvatar' +import {PreviewableUserAvatar} from '../util/UserAvatar' import {s} from 'lib/styles' import {ago, niceDate} from 'lib/strings/time' import {sanitizeDisplayName} from 'lib/strings/display-names' @@ -163,22 +163,17 @@ export const PostThreadItem = observer(function PostThreadItem({ - - - + - + - - - + {replyAuthorDid !== '' && ( diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 921f23190..5035d345d 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -28,7 +28,6 @@ const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} export const Feed = observer(function Feed({ feed, style, - showPostFollowBtn, scrollElRef, onPressTryAgain, onScroll, @@ -41,7 +40,6 @@ export const Feed = observer(function Feed({ }: { feed: PostsFeedModel style?: StyleProp - showPostFollowBtn?: boolean scrollElRef?: MutableRefObject | null> onPressTryAgain?: () => void onScroll?: OnScrollCb @@ -138,15 +136,9 @@ export const Feed = observer(function Feed({ } else if (item === LOADING_ITEM) { return } - return + return }, - [ - feed, - onPressTryAgain, - onPressRetryLoadMore, - showPostFollowBtn, - renderEmptyState, - ], + [feed, onPressTryAgain, onPressRetryLoadMore, renderEmptyState], ) const FeedFooter = React.useCallback( diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 6ec2c80f4..e1b160dcb 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -21,7 +21,7 @@ import {ImageHider} from '../util/moderation/ImageHider' import {RichText} from '../util/text/RichText' import {PostSandboxWarning} from '../util/PostSandboxWarning' import * as Toast from '../util/Toast' -import {UserAvatar} from '../util/UserAvatar' +import {PreviewableUserAvatar} from '../util/UserAvatar' import {s} from 'lib/styles' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' @@ -33,14 +33,12 @@ export const FeedItem = observer(function ({ item, isThreadChild, isThreadParent, - showFollowBtn, ignoreMuteFor, }: { item: PostsFeedItemModel isThreadChild?: boolean isThreadParent?: boolean showReplyLine?: boolean - showFollowBtn?: boolean ignoreMuteFor?: string }) { const store = useStores() @@ -55,7 +53,6 @@ export const FeedItem = observer(function ({ return `/profile/${item.post.author.handle}/post/${urip.rkey}` }, [item.post.uri, item.post.author.handle]) const itemTitle = `Post by ${item.post.author.handle}` - const authorHref = `/profile/${item.post.author.handle}` const replyAuthorDid = useMemo(() => { if (!record?.reply) { return '' @@ -214,13 +211,13 @@ export const FeedItem = observer(function ({ - - - + {!isThreadChild && replyAuthorDid !== '' && ( @@ -357,9 +352,9 @@ const styles = StyleSheet.create({ layout: { flexDirection: 'row', marginTop: 1, + gap: 10, }, layoutAvi: { - width: 70, paddingLeft: 8, }, layoutContent: { diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx index d75ff1385..8ac813b92 100644 --- a/src/view/com/posts/FeedSlice.tsx +++ b/src/view/com/posts/FeedSlice.tsx @@ -11,11 +11,9 @@ import {ModerationBehaviorCode} from 'lib/labeling/types' export function FeedSlice({ slice, - showFollowBtn, ignoreMuteFor, }: { slice: PostsFeedSliceModel - showFollowBtn?: boolean ignoreMuteFor?: string }) { if (slice.moderation.list.behavior === ModerationBehaviorCode.Hide) { @@ -32,7 +30,6 @@ export function FeedSlice({ item={slice.items[0]} isThreadParent={slice.isThreadParentAt(0)} isThreadChild={slice.isThreadChildAt(0)} - showFollowBtn={showFollowBtn} ignoreMuteFor={ignoreMuteFor} /> @@ -49,7 +45,6 @@ export function FeedSlice({ item={slice.items[last]} isThreadParent={slice.isThreadParentAt(last)} isThreadChild={slice.isThreadChildAt(last)} - showFollowBtn={showFollowBtn} ignoreMuteFor={ignoreMuteFor} /> @@ -64,7 +59,6 @@ export function FeedSlice({ item={item} isThreadParent={slice.isThreadParentAt(i)} isThreadChild={slice.isThreadChildAt(i)} - showFollowBtn={showFollowBtn} ignoreMuteFor={ignoreMuteFor} /> ))} diff --git a/src/view/com/posts/MultiFeed.tsx b/src/view/com/posts/MultiFeed.tsx index 466a7a47d..97899e554 100644 --- a/src/view/com/posts/MultiFeed.tsx +++ b/src/view/com/posts/MultiFeed.tsx @@ -28,7 +28,6 @@ import {CogIcon} from 'lib/icons' export const MultiFeed = observer(function Feed({ multifeed, style, - showPostFollowBtn, scrollElRef, onScroll, scrollEventThrottle, @@ -38,7 +37,6 @@ export const MultiFeed = observer(function Feed({ }: { multifeed: PostsMultiFeedModel style?: StyleProp - showPostFollowBtn?: boolean scrollElRef?: MutableRefObject | null> onPressTryAgain?: () => void onScroll?: OnScrollCb @@ -105,9 +103,7 @@ export const MultiFeed = observer(function Feed({ ) } else if (item.type === 'feed-slice') { - return ( - - ) + return } else if (item.type === 'feed-loading') { return } else if (item.type === 'feed-error') { @@ -139,7 +135,7 @@ export const MultiFeed = observer(function Feed({ } return null }, - [showPostFollowBtn, pal], + [pal], ) const ListFooter = React.useCallback( diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index b142e7616..46ff3d979 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -6,10 +6,7 @@ import { TouchableWithoutFeedback, View, } from 'react-native' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {useNavigation} from '@react-navigation/native' import {BlurView} from '../util/BlurView' import {ProfileModel} from 'state/models/content/profile' @@ -102,6 +99,7 @@ export const ProfileHeader = observer( const ProfileHeaderLoaded = observer( ({view, onRefreshAll, hideBackButton = false}: Props) => { const pal = usePalette('default') + const palInverted = usePalette('inverted') const store = useStores() const navigation = useNavigation() const {track} = useAnalytics() @@ -351,15 +349,15 @@ const ProfileHeaderLoaded = observer( - + Follow @@ -609,7 +607,6 @@ const styles = StyleSheet.create({ }, description: { - flex: 1, marginBottom: 8, }, diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 1dec97e78..454fd7c21 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -6,6 +6,7 @@ import { Platform, StyleProp, TextStyle, + TextProps, View, ViewStyle, TouchableOpacity, @@ -144,7 +145,7 @@ export const TextLink = observer(function TextLink({ numberOfLines?: number lineHeight?: number dataSet?: any -}) { +} & TextProps) { const {...props} = useLinkProps({to: sanitizeUrl(href)}) const store = useStores() const navigation = useNavigation() @@ -186,16 +187,7 @@ export const TextLink = observer(function TextLink({ /** * Only acts as a link on desktop web */ -export const DesktopWebTextLink = observer(function DesktopWebTextLink({ - testID, - type = 'md', - style, - href, - text, - numberOfLines, - lineHeight, - ...props -}: { +interface DesktopWebTextLinkProps extends TextProps { testID?: string type?: TypographyVariant style?: StyleProp @@ -206,7 +198,17 @@ export const DesktopWebTextLink = observer(function DesktopWebTextLink({ accessible?: boolean accessibilityLabel?: string accessibilityHint?: string -}) { +} +export const DesktopWebTextLink = observer(function DesktopWebTextLink({ + testID, + type = 'md', + style, + href, + text, + numberOfLines, + lineHeight, + ...props +}: DesktopWebTextLinkProps) { if (isDesktopWeb) { return ( { - setDidFollow(true) - }, [setDidFollow]) - - if ( - opts.showFollowBtn && - !isMe && - (followState === FollowState.NotFollowing || didFollow) && - opts.did - ) { - // two-liner with follow button - return ( - - - - - -  ·  - - - - - - - - - - - ) - } - - // one-liner return ( - + {typeof opts.authorAvatar !== 'undefined' && ( - + )} - + - - ·  - + {!isAndroid && ( + + · + + )} void +} + +interface PreviewableUserAvatarProps extends BaseUserAvatarProps { + did: string + handle: string +} + const BLUR_AMOUNT = isWeb ? 5 : 100 function DefaultAvatar({type, size}: {type: Type; size: number}) { @@ -91,13 +109,7 @@ export function UserAvatar({ avatar, moderation, onSelectNewAvatar, -}: { - type?: Type - size: number - avatar?: string | null - moderation?: AvatarModeration - onSelectNewAvatar?: (img: RNImage | null) => void -}) { +}: UserAvatarProps) { const store = useStores() const pal = usePalette('default') const {requestCameraAccessIfNeeded} = useCameraPermission() @@ -244,6 +256,32 @@ export function UserAvatar({ ) } +export function PreviewableUserAvatar(props: PreviewableUserAvatarProps) { + const store = useStores() + + if (isDesktopWeb) { + return ( + + + + ) + } + return ( + + store.shell.openModal({ + name: 'profile-preview', + did: props.did, + }) + } + accessibilityRole="button" + accessibilityLabel={props.handle} + accessibilityHint=""> + + + ) +} + const styles = StyleSheet.create({ editButtonContainer: { position: 'absolute', diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx index 7d4452384..1ab59f736 100644 --- a/src/view/screens/Feeds.tsx +++ b/src/view/screens/Feeds.tsx @@ -106,7 +106,6 @@ export const FeedsScreen = withAuthRequired( onScroll={onMainScroll} scrollEventThrottle={100} headerOffset={HEADER_OFFSET} - showPostFollowBtn />