about summary refs log tree commit diff
path: root/src/view/com/posts/FeedItem.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/posts/FeedItem.tsx')
-rw-r--r--src/view/com/posts/FeedItem.tsx273
1 files changed, 116 insertions, 157 deletions
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index aeee3e20a..dfb0cfcf6 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -1,14 +1,17 @@
-import React, {useMemo, useState} from 'react'
-import {observer} from 'mobx-react-lite'
-import {Linking, StyleSheet, View} from 'react-native'
-import Clipboard from '@react-native-clipboard/clipboard'
-import {AtUri} from '@atproto/api'
+import React, {memo, useMemo, useState} from 'react'
+import {StyleSheet, View} from 'react-native'
+import {
+  AppBskyFeedDefs,
+  AppBskyFeedPost,
+  AtUri,
+  PostModeration,
+  RichText as RichTextAPI,
+} from '@atproto/api'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
-import {PostsFeedItemModel} from 'state/models/feeds/post'
-import {FeedSourceInfo} from 'lib/api/feed/types'
+import {ReasonFeedSource, isReasonFeedSource} from 'lib/api/feed/types'
 import {Link, TextLinkOnWebOnly, TextLink} from '../util/Link'
 import {Text} from '../util/text/Text'
 import {UserInfoText} from '../util/UserInfoText'
@@ -19,50 +22,96 @@ import {ContentHider} from '../util/moderation/ContentHider'
 import {PostAlerts} from '../util/moderation/PostAlerts'
 import {RichText} from '../util/text/RichText'
 import {PostSandboxWarning} from '../util/PostSandboxWarning'
-import * as Toast from '../util/Toast'
 import {PreviewableUserAvatar} from '../util/UserAvatar'
 import {s} from 'lib/styles'
-import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
-import {getTranslatorLink} from '../../../locale/helpers'
 import {makeProfileLink} from 'lib/routes/links'
 import {isEmbedByEmbedder} from 'lib/embeds'
 import {MAX_POST_LINES} from 'lib/constants'
 import {countLines} from 'lib/strings/helpers'
-import {logger} from '#/logger'
+import {useComposerControls} from '#/state/shell/composer'
+import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
 
-export const FeedItem = observer(function FeedItemImpl({
-  item,
-  source,
+export function FeedItem({
+  post,
+  record,
+  reason,
+  moderation,
   isThreadChild,
   isThreadLastChild,
   isThreadParent,
 }: {
-  item: PostsFeedItemModel
-  source?: FeedSourceInfo
+  post: AppBskyFeedDefs.PostView
+  record: AppBskyFeedPost.Record
+  reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined
+  moderation: PostModeration
   isThreadChild?: boolean
   isThreadLastChild?: boolean
   isThreadParent?: boolean
-  showReplyLine?: boolean
 }) {
-  const store = useStores()
+  const postShadowed = usePostShadow(post)
+  const richText = useMemo(
+    () =>
+      new RichTextAPI({
+        text: record.text,
+        facets: record.facets,
+      }),
+    [record],
+  )
+  if (postShadowed === POST_TOMBSTONE) {
+    return null
+  }
+  if (richText && moderation) {
+    return (
+      <FeedItemInner
+        post={postShadowed}
+        record={record}
+        reason={reason}
+        richText={richText}
+        moderation={moderation}
+        isThreadChild={isThreadChild}
+        isThreadLastChild={isThreadLastChild}
+        isThreadParent={isThreadParent}
+      />
+    )
+  }
+  return null
+}
+
+let FeedItemInner = ({
+  post,
+  record,
+  reason,
+  richText,
+  moderation,
+  isThreadChild,
+  isThreadLastChild,
+  isThreadParent,
+}: {
+  post: Shadow<AppBskyFeedDefs.PostView>
+  record: AppBskyFeedPost.Record
+  reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined
+  richText: RichTextAPI
+  moderation: PostModeration
+  isThreadChild?: boolean
+  isThreadLastChild?: boolean
+  isThreadParent?: boolean
+}): React.ReactNode => {
+  const {openComposer} = useComposerControls()
   const pal = usePalette('default')
   const {track} = useAnalytics()
-  const [deleted, setDeleted] = useState(false)
   const [limitLines, setLimitLines] = useState(
-    countLines(item.richText?.text) >= MAX_POST_LINES,
+    () => countLines(richText.text) >= MAX_POST_LINES,
   )
-  const record = item.postRecord
-  const itemUri = item.post.uri
-  const itemCid = item.post.cid
-  const itemHref = useMemo(() => {
-    const urip = new AtUri(item.post.uri)
-    return makeProfileLink(item.post.author, 'post', urip.rkey)
-  }, [item.post.uri, item.post.author])
-  const itemTitle = `Post by ${item.post.author.handle}`
+
+  const href = useMemo(() => {
+    const urip = new AtUri(post.uri)
+    return makeProfileLink(post.author, 'post', urip.rkey)
+  }, [post.uri, post.author])
+
   const replyAuthorDid = useMemo(() => {
     if (!record?.reply) {
       return ''
@@ -70,77 +119,22 @@ export const FeedItem = observer(function FeedItemImpl({
     const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri)
     return urip.hostname
   }, [record?.reply])
-  const translatorUrl = getTranslatorLink(
-    record?.text || '',
-    store.preferences.primaryLanguage,
-  )
 
   const onPressReply = React.useCallback(() => {
     track('FeedItem:PostReply')
-    store.shell.openComposer({
+    openComposer({
       replyTo: {
-        uri: item.post.uri,
-        cid: item.post.cid,
-        text: record?.text || '',
+        uri: post.uri,
+        cid: post.cid,
+        text: record.text || '',
         author: {
-          handle: item.post.author.handle,
-          displayName: item.post.author.displayName,
-          avatar: item.post.author.avatar,
+          handle: post.author.handle,
+          displayName: post.author.displayName,
+          avatar: post.author.avatar,
         },
       },
     })
-  }, [item, track, record, store])
-
-  const onPressToggleRepost = React.useCallback(() => {
-    track('FeedItem:PostRepost')
-    return item
-      .toggleRepost()
-      .catch(e => logger.error('Failed to toggle repost', {error: e}))
-  }, [track, item])
-
-  const onPressToggleLike = React.useCallback(() => {
-    track('FeedItem:PostLike')
-    return item
-      .toggleLike()
-      .catch(e => logger.error('Failed to toggle like', {error: e}))
-  }, [track, item])
-
-  const onCopyPostText = React.useCallback(() => {
-    Clipboard.setString(record?.text || '')
-    Toast.show('Copied to clipboard')
-  }, [record])
-
-  const onOpenTranslate = React.useCallback(() => {
-    Linking.openURL(translatorUrl)
-  }, [translatorUrl])
-
-  const onToggleThreadMute = React.useCallback(async () => {
-    track('FeedItem:ThreadMute')
-    try {
-      await item.toggleThreadMute()
-      if (item.isThreadMuted) {
-        Toast.show('You will no longer receive notifications for this thread')
-      } else {
-        Toast.show('You will now receive notifications for this thread')
-      }
-    } catch (e) {
-      logger.error('Failed to toggle thread mute', {error: e})
-    }
-  }, [track, item])
-
-  const onDeletePost = React.useCallback(() => {
-    track('FeedItem:PostDelete')
-    item.delete().then(
-      () => {
-        setDeleted(true)
-        Toast.show('Post deleted')
-      },
-      e => {
-        logger.error('Failed to delete post', {error: e})
-        Toast.show('Failed to delete post, please try again')
-      },
-    )
-  }, [track, item, setDeleted])
+  }, [post, record, track, openComposer])
 
   const onPressShowMore = React.useCallback(() => {
     setLimitLines(false)
@@ -159,15 +153,11 @@ export const FeedItem = observer(function FeedItemImpl({
     isThreadChild ? styles.outerSmallTop : undefined,
   ]
 
-  if (!record || deleted) {
-    return <View />
-  }
-
   return (
     <Link
-      testID={`feedItem-by-${item.post.author.handle}`}
+      testID={`feedItem-by-${post.author.handle}`}
       style={outerStyles}
-      href={itemHref}
+      href={href}
       noFeedback
       accessible={false}>
       <PostSandboxWarning />
@@ -189,10 +179,10 @@ export const FeedItem = observer(function FeedItemImpl({
         </View>
 
         <View style={{paddingTop: 12, flexShrink: 1}}>
-          {source ? (
+          {isReasonFeedSource(reason) ? (
             <Link
-              title={sanitizeDisplayName(source.displayName)}
-              href={source.uri}>
+              title={sanitizeDisplayName(reason.displayName)}
+              href={reason.uri}>
               <Text
                 type="sm-bold"
                 style={pal.textLight}
@@ -204,17 +194,17 @@ export const FeedItem = observer(function FeedItemImpl({
                   style={pal.textLight}
                   lineHeight={1.2}
                   numberOfLines={1}
-                  text={sanitizeDisplayName(source.displayName)}
-                  href={source.uri}
+                  text={sanitizeDisplayName(reason.displayName)}
+                  href={reason.uri}
                 />
               </Text>
             </Link>
-          ) : item.reasonRepost ? (
+          ) : AppBskyFeedDefs.isReasonRepost(reason) ? (
             <Link
               style={styles.includeReason}
-              href={makeProfileLink(item.reasonRepost.by)}
+              href={makeProfileLink(reason.by)}
               title={`Reposted by ${sanitizeDisplayName(
-                item.reasonRepost.by.displayName || item.reasonRepost.by.handle,
+                reason.by.displayName || reason.by.handle,
               )}`}>
               <FontAwesomeIcon
                 icon="retweet"
@@ -236,10 +226,9 @@ export const FeedItem = observer(function FeedItemImpl({
                   lineHeight={1.2}
                   numberOfLines={1}
                   text={sanitizeDisplayName(
-                    item.reasonRepost.by.displayName ||
-                      sanitizeHandle(item.reasonRepost.by.handle),
+                    reason.by.displayName || sanitizeHandle(reason.by.handle),
                   )}
-                  href={makeProfileLink(item.reasonRepost.by)}
+                  href={makeProfileLink(reason.by)}
                 />
               </Text>
             </Link>
@@ -251,10 +240,10 @@ export const FeedItem = observer(function FeedItemImpl({
         <View style={styles.layoutAvi}>
           <PreviewableUserAvatar
             size={52}
-            did={item.post.author.did}
-            handle={item.post.author.handle}
-            avatar={item.post.author.avatar}
-            moderation={item.moderation.avatar}
+            did={post.author.did}
+            handle={post.author.handle}
+            avatar={post.author.avatar}
+            moderation={moderation.avatar}
           />
           {isThreadParent && (
             <View
@@ -271,10 +260,10 @@ export const FeedItem = observer(function FeedItemImpl({
         </View>
         <View style={styles.layoutContent}>
           <PostMeta
-            author={item.post.author}
-            authorHasWarning={!!item.post.author.labels?.length}
-            timestamp={item.post.indexedAt}
-            postHref={itemHref}
+            author={post.author}
+            authorHasWarning={!!post.author.labels?.length}
+            timestamp={post.indexedAt}
+            postHref={href}
           />
           {!isThreadChild && replyAuthorDid !== '' && (
             <View style={[s.flexRow, s.mb2, s.alignCenter]}>
@@ -303,19 +292,16 @@ export const FeedItem = observer(function FeedItemImpl({
           )}
           <ContentHider
             testID="contentHider-post"
-            moderation={item.moderation.content}
+            moderation={moderation.content}
             ignoreMute
             childContainerStyle={styles.contentHiderChild}>
-            <PostAlerts
-              moderation={item.moderation.content}
-              style={styles.alert}
-            />
-            {item.richText?.text ? (
+            <PostAlerts moderation={moderation.content} style={styles.alert} />
+            {richText.text ? (
               <View style={styles.postTextContainer}>
                 <RichText
                   testID="postText"
                   type="post-text"
-                  richText={item.richText}
+                  richText={richText}
                   lineHeight={1.3}
                   numberOfLines={limitLines ? MAX_POST_LINES : undefined}
                   style={s.flex1}
@@ -330,50 +316,23 @@ export const FeedItem = observer(function FeedItemImpl({
                 href="#"
               />
             ) : undefined}
-            {item.post.embed ? (
+            {post.embed ? (
               <ContentHider
                 testID="contentHider-embed"
-                moderation={item.moderation.embed}
-                ignoreMute={isEmbedByEmbedder(
-                  item.post.embed,
-                  item.post.author.did,
-                )}
+                moderation={moderation.embed}
+                ignoreMute={isEmbedByEmbedder(post.embed, post.author.did)}
                 style={styles.embed}>
-                <PostEmbeds
-                  embed={item.post.embed}
-                  moderation={item.moderation.embed}
-                />
+                <PostEmbeds embed={post.embed} moderation={moderation.embed} />
               </ContentHider>
             ) : null}
           </ContentHider>
-          <PostCtrls
-            itemUri={itemUri}
-            itemCid={itemCid}
-            itemHref={itemHref}
-            itemTitle={itemTitle}
-            author={item.post.author}
-            text={item.richText?.text || record.text}
-            indexedAt={item.post.indexedAt}
-            isAuthor={item.post.author.did === store.me.did}
-            replyCount={item.post.replyCount}
-            repostCount={item.post.repostCount}
-            likeCount={item.post.likeCount}
-            isReposted={!!item.post.viewer?.repost}
-            isLiked={!!item.post.viewer?.like}
-            isThreadMuted={item.isThreadMuted}
-            onPressReply={onPressReply}
-            onPressToggleRepost={onPressToggleRepost}
-            onPressToggleLike={onPressToggleLike}
-            onCopyPostText={onCopyPostText}
-            onOpenTranslate={onOpenTranslate}
-            onToggleThreadMute={onToggleThreadMute}
-            onDeletePost={onDeletePost}
-          />
+          <PostCtrls post={post} record={record} onPressReply={onPressReply} />
         </View>
       </View>
     </Link>
   )
-})
+}
+FeedItemInner = memo(FeedItemInner)
 
 const styles = StyleSheet.create({
   outer: {