about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/analytics/types.ts1
-rw-r--r--src/state/models/ui/shell.ts6
-rw-r--r--src/view/com/modals/Modal.tsx4
-rw-r--r--src/view/com/modals/Modal.web.tsx3
-rw-r--r--src/view/com/modals/ProfilePreview.tsx89
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx38
-rw-r--r--src/view/com/post/Post.tsx1
-rw-r--r--src/view/com/posts/Feed.tsx12
-rw-r--r--src/view/com/posts/FeedItem.tsx23
-rw-r--r--src/view/com/posts/FeedSlice.tsx6
-rw-r--r--src/view/com/posts/MultiFeed.tsx8
-rw-r--r--src/view/com/profile/ProfileHeader.tsx13
-rw-r--r--src/view/com/util/Link.tsx26
-rw-r--r--src/view/com/util/PostMeta.tsx119
-rw-r--r--src/view/com/util/UserAvatar.tsx54
-rw-r--r--src/view/screens/Feeds.tsx1
-rw-r--r--src/view/screens/Home.tsx1
17 files changed, 215 insertions, 190 deletions
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 = <EditProfileModal.Component {...activeModal} />
+  } else if (activeModal?.name === 'profile-preview') {
+    snapPoints = ProfilePreviewModal.snapPoints
+    element = <ProfilePreviewModal.Component {...activeModal} />
   } else if (activeModal?.name === 'server-input') {
     snapPoints = ServerInputModal.snapPoints
     element = <ServerInputModal.Component {...activeModal} />
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 = <ConfirmModal.Component {...modal} />
   } else if (modal.name === 'edit-profile') {
     element = <EditProfileModal.Component {...modal} />
+  } else if (modal.name === 'profile-preview') {
+    element = <ProfilePreviewModal.Component {...modal} />
   } else if (modal.name === 'server-input') {
     element = <ServerInputModal.Component {...modal} />
   } 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<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,
+  },
+})
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({
         <PostSandboxWarning />
         <View style={styles.layout}>
           <View style={styles.layoutAvi}>
-            <Link
-              href={authorHref}
-              title={authorTitle}
-              asAnchor
-              accessibilityLabel={`${item.post.author.handle}'s avatar`}
-              accessibilityHint="">
-              <UserAvatar
-                size={52}
-                avatar={item.post.author.avatar}
-                moderation={item.moderation.avatar}
-              />
-            </Link>
+            <PreviewableUserAvatar
+              size={52}
+              did={item.post.author.did}
+              handle={item.post.author.handle}
+              avatar={item.post.author.avatar}
+              moderation={item.moderation.avatar}
+            />
           </View>
           <View style={styles.layoutContent}>
             <View style={[styles.meta, styles.metaExpandedLine1]}>
-              <View style={[s.flexRow, s.alignBaseline]}>
+              <View style={[s.flexRow]}>
                 <Link
                   style={styles.metaItem}
                   href={authorHref}
@@ -353,13 +348,13 @@ export const PostThreadItem = observer(function PostThreadItem({
           <PostSandboxWarning />
           <View style={styles.layout}>
             <View style={styles.layoutAvi}>
-              <Link href={authorHref} title={authorTitle} asAnchor>
-                <UserAvatar
-                  size={52}
-                  avatar={item.post.author.avatar}
-                  moderation={item.moderation.avatar}
-                />
-              </Link>
+              <PreviewableUserAvatar
+                size={52}
+                did={item.post.author.did}
+                handle={item.post.author.handle}
+                avatar={item.post.author.avatar}
+                moderation={item.moderation.avatar}
+              />
             </View>
             <View style={styles.layoutContent}>
               <PostMeta
@@ -368,7 +363,6 @@ export const PostThreadItem = observer(function PostThreadItem({
                 authorHasWarning={!!item.post.author.labels?.length}
                 timestamp={item.post.indexedAt}
                 postHref={itemHref}
-                did={item.post.author.did}
               />
               <ContentHider
                 moderation={item.moderation.thread}
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index c380c9743..34154e7ed 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -229,7 +229,6 @@ const PostLoaded = observer(
               authorHasWarning={!!item.post.author.labels?.length}
               timestamp={item.post.indexedAt}
               postHref={itemHref}
-              did={item.post.author.did}
             />
             {replyAuthorDid !== '' && (
               <View style={[s.flexRow, s.mb2, s.alignCenter]}>
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<ViewStyle>
-  showPostFollowBtn?: boolean
   scrollElRef?: MutableRefObject<FlatList<any> | null>
   onPressTryAgain?: () => void
   onScroll?: OnScrollCb
@@ -138,15 +136,9 @@ export const Feed = observer(function Feed({
       } else if (item === LOADING_ITEM) {
         return <PostFeedLoadingPlaceholder />
       }
-      return <FeedSlice slice={item} showFollowBtn={showPostFollowBtn} />
+      return <FeedSlice slice={item} />
     },
-    [
-      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 ({
       <PostSandboxWarning />
       <View style={styles.layout}>
         <View style={styles.layoutAvi}>
-          <Link href={authorHref} title={item.post.author.handle} asAnchor>
-            <UserAvatar
-              size={52}
-              avatar={item.post.author.avatar}
-              moderation={item.moderation.avatar}
-            />
-          </Link>
+          <PreviewableUserAvatar
+            size={52}
+            did={item.post.author.did}
+            handle={item.post.author.handle}
+            avatar={item.post.author.avatar}
+            moderation={item.moderation.avatar}
+          />
         </View>
         <View style={styles.layoutContent}>
           <PostMeta
@@ -229,8 +226,6 @@ export const FeedItem = observer(function ({
             authorHasWarning={!!item.post.author.labels?.length}
             timestamp={item.post.indexedAt}
             postHref={itemHref}
-            did={item.post.author.did}
-            showFollowBtn={showFollowBtn}
           />
           {!isThreadChild && replyAuthorDid !== '' && (
             <View style={[s.flexRow, s.mb2, s.alignCenter]}>
@@ -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}
         />
         <FeedItem
@@ -40,7 +37,6 @@ export function FeedSlice({
           item={slice.items[1]}
           isThreadParent={slice.isThreadParentAt(1)}
           isThreadChild={slice.isThreadChildAt(1)}
-          showFollowBtn={showFollowBtn}
           ignoreMuteFor={ignoreMuteFor}
         />
         <ViewFullThread slice={slice} />
@@ -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<ViewStyle>
-  showPostFollowBtn?: boolean
   scrollElRef?: MutableRefObject<FlatList<any> | null>
   onPressTryAgain?: () => void
   onScroll?: OnScrollCb
@@ -105,9 +103,7 @@ export const MultiFeed = observer(function Feed({
           </View>
         )
       } else if (item.type === 'feed-slice') {
-        return (
-          <FeedSlice slice={item.slice} showFollowBtn={showPostFollowBtn} />
-        )
+        return <FeedSlice slice={item.slice} />
       } else if (item.type === 'feed-loading') {
         return <PostFeedLoadingPlaceholder />
       } 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<NavigationProp>()
     const {track} = useAnalytics()
@@ -351,15 +349,15 @@ const ProfileHeaderLoaded = observer(
                   <TouchableOpacity
                     testID="followBtn"
                     onPress={onPressToggleFollow}
-                    style={[styles.btn, styles.primaryBtn]}
+                    style={[styles.btn, styles.mainBtn, palInverted.view]}
                     accessibilityRole="button"
                     accessibilityLabel={`Follow ${view.handle}`}
                     accessibilityHint={`Shows direct posts from ${view.handle} in your feed`}>
                     <FontAwesomeIcon
                       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
                     </Text>
                   </TouchableOpacity>
@@ -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<NavigationProp>()
@@ -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<TextStyle>
@@ -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 (
       <TextLink
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index 628c88722..396b0278d 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -4,12 +4,10 @@ import {Text} from './text/Text'
 import {DesktopWebTextLink} from './Link'
 import {ago, niceDate} from 'lib/strings/time'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useStores} from 'state/index'
 import {UserAvatar} from './UserAvatar'
 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 {isAndroid, isIOS} from 'platform/detection'
 
 interface PostMetaOpts {
   authorAvatar?: string
@@ -18,88 +16,17 @@ interface PostMetaOpts {
   authorHasWarning: boolean
   postHref: string
   timestamp: string
-  did?: string
-  showFollowBtn?: boolean
 }
 
 export const PostMeta = observer(function (opts: PostMetaOpts) {
   const pal = usePalette('default')
   const displayName = opts.authorDisplayName || 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 (
-    <View style={styles.meta}>
+    <View style={styles.metaOneLine}>
       {typeof opts.authorAvatar !== 'undefined' && (
-        <View style={[styles.metaItem, styles.avatar]}>
+        <View style={styles.avatar}>
           <UserAvatar
             avatar={opts.authorAvatar}
             size={16}
@@ -107,7 +34,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
           />
         </View>
       )}
-      <View style={[styles.metaItem, styles.maxWidth]}>
+      <View style={styles.maxWidth}>
         <DesktopWebTextLink
           type="lg-bold"
           style={pal.text}
@@ -128,12 +55,18 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
           href={`/profile/${opts.authorHandle}`}
         />
       </View>
-      <Text type="md" style={pal.textLight} lineHeight={1.2} accessible={false}>
-        &middot;&nbsp;
-      </Text>
+      {!isAndroid && (
+        <Text
+          type="md"
+          style={pal.textLight}
+          lineHeight={1.2}
+          accessible={false}>
+          &middot;
+        </Text>
+      )}
       <DesktopWebTextLink
         type="md"
-        style={[styles.metaItem, pal.textLight]}
+        style={pal.textLight}
         lineHeight={1.2}
         text={ago(opts.timestamp)}
         accessibilityLabel={niceDate(opts.timestamp)}
@@ -145,32 +78,16 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
 })
 
 const styles = StyleSheet.create({
-  meta: {
+  metaOneLine: {
     flexDirection: 'row',
     paddingBottom: 2,
-  },
-  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,
+    gap: 4,
   },
   avatar: {
     alignSelf: 'center',
   },
   maxWidth: {
-    maxWidth: '80%',
+    flex: isAndroid ? 1 : undefined,
+    maxWidth: isIOS ? '80%' : undefined,
   },
 })
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index b94cf54e9..135615a3b 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -1,5 +1,5 @@
 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 {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {IconProp} from '@fortawesome/fontawesome-svg-core'
@@ -12,13 +12,31 @@ import {
 import {useStores} from 'state/index'
 import {colors} from 'lib/styles'
 import {DropdownButton} from './forms/DropdownButton'
+import {Link} from './Link'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb, isAndroid} from 'platform/detection'
 import {Image as RNImage} from 'react-native-image-crop-picker'
 import {AvatarModeration} from 'lib/labeling/types'
+import {isDesktopWeb} from 'platform/detection'
 
 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
 
 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 (
+      <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({
   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
         />
         <ViewHeader
           title="My Feeds"
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index ff2b2a0bd..41459cfa5 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -266,7 +266,6 @@ const FeedPage = observer(
           key="default"
           feed={feed}
           scrollElRef={scrollElRef}
-          showPostFollowBtn
           onPressTryAgain={onPressTryAgain}
           onScroll={onMainScroll}
           scrollEventThrottle={100}