From 6471e809aa28f0319bde4aa1f362679e3723d298 Mon Sep 17 00:00:00 2001
From: Samuel Newman <mozzius@protonmail.com>
Date: Tue, 12 Nov 2024 21:02:41 +0000
Subject: Add backdated post indicator (#6156)

* add backdate indicator

* pill style

* add indexedAt

* update indicator - new copy, date in pill

* complete alf migration

* accidentally committed the missing indexedAt *again*!

* copy tweak
---
 src/view/com/post-thread/PostThreadItem.tsx | 233 ++++++++++++++++++++--------
 1 file changed, 165 insertions(+), 68 deletions(-)

(limited to 'src/view/com/post-thread/PostThreadItem.tsx')

diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 5044f9621..9edca4335 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -1,5 +1,5 @@
 import React, {memo, useMemo} from 'react'
-import {StyleSheet, View} from 'react-native'
+import {StyleSheet, Text as RNText, View} from 'react-native'
 import {
   AppBskyFeedDefs,
   AppBskyFeedPost,
@@ -8,7 +8,6 @@ import {
   ModerationDecision,
   RichText as RichTextAPI,
 } from '@atproto/api'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {msg, Plural, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
@@ -21,6 +20,7 @@ import {sanitizeHandle} from '#/lib/strings/handles'
 import {countLines} from '#/lib/strings/helpers'
 import {niceDate} from '#/lib/strings/time'
 import {s} from '#/lib/styles'
+import {getTranslatorLink, isPostInLanguage} from '#/locale/helpers'
 import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
 import {useLanguagePrefs} from '#/state/preferences'
 import {ThreadPost} from '#/state/queries/post-thread'
@@ -28,26 +28,30 @@ import {useSession} from '#/state/session'
 import {useComposerControls} from '#/state/shell/composer'
 import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
 import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn'
+import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
+import {Link, TextLink} from '#/view/com/util/Link'
+import {formatCount} from '#/view/com/util/numeric/format'
+import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls'
+import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds'
+import {PostMeta} from '#/view/com/util/PostMeta'
+import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
 import {atoms as a, useTheme} from '#/alf'
+import {colors} from '#/components/Admonition'
+import {Button} from '#/components/Button'
+import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock'
+import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
+import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
 import {InlineLinkText} from '#/components/Link'
+import {ContentHider} from '#/components/moderation/ContentHider'
+import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
+import {PostAlerts} from '#/components/moderation/PostAlerts'
+import {PostHider} from '#/components/moderation/PostHider'
 import {AppModerationCause} from '#/components/Pills'
+import * as Prompt from '#/components/Prompt'
 import {RichText} from '#/components/RichText'
 import {SubtleWebHover} from '#/components/SubtleWebHover'
-import {Text as NewText} from '#/components/Typography'
-import {ContentHider} from '../../../components/moderation/ContentHider'
-import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
-import {PostAlerts} from '../../../components/moderation/PostAlerts'
-import {PostHider} from '../../../components/moderation/PostHider'
-import {WhoCanReply} from '../../../components/WhoCanReply'
-import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
-import {ErrorMessage} from '../util/error/ErrorMessage'
-import {Link, TextLink} from '../util/Link'
-import {formatCount} from '../util/numeric/format'
-import {PostCtrls} from '../util/post-ctrls/PostCtrls'
-import {PostEmbeds, PostEmbedViewContext} from '../util/post-embeds'
-import {PostMeta} from '../util/PostMeta'
-import {Text} from '../util/text/Text'
-import {PreviewableUserAvatar} from '../util/UserAvatar'
+import {Text} from '#/components/Typography'
+import {WhoCanReply} from '#/components/WhoCanReply'
 
 export function PostThreadItem({
   post,
@@ -125,19 +129,20 @@ export function PostThreadItem({
 }
 
 function PostThreadItemDeleted({hideTopBorder}: {hideTopBorder?: boolean}) {
-  const pal = usePalette('default')
+  const t = useTheme()
   return (
     <View
       style={[
-        styles.outer,
-        pal.border,
-        pal.view,
-        s.p20,
-        s.flexRow,
-        hideTopBorder && styles.noTopBorder,
+        t.atoms.bg,
+        t.atoms.border_contrast_low,
+        a.p_xl,
+        a.pl_lg,
+        a.flex_row,
+        a.gap_md,
+        !hideTopBorder && a.border_t,
       ]}>
-      <FontAwesomeIcon icon={['far', 'trash-can']} color={pal.colors.icon} />
-      <Text style={[pal.textLight, s.ml10]}>
+      <TrashIcon style={[t.atoms.text]} />
+      <Text style={[t.atoms.text_contrast_medium, a.mt_2xs]}>
         <Trans>This post has been deleted.</Trans>
       </Text>
     </View>
@@ -308,7 +313,7 @@ let PostThreadItemLoaded = ({
             />
             <View style={[a.flex_1]}>
               <Link style={s.flex1} href={authorHref} title={authorTitle}>
-                <NewText
+                <Text
                   emoji
                   style={[a.text_lg, a.font_bold, a.leading_snug, a.self_start]}
                   numberOfLines={1}>
@@ -317,10 +322,10 @@ let PostThreadItemLoaded = ({
                       sanitizeHandle(post.author.handle),
                     moderation.ui('displayName'),
                   )}
-                </NewText>
+                </Text>
               </Link>
               <Link style={s.flex1} href={authorHref} title={authorTitle}>
-                <NewText
+                <Text
                   emoji
                   style={[
                     a.text_md,
@@ -329,7 +334,7 @@ let PostThreadItemLoaded = ({
                   ]}
                   numberOfLines={1}>
                   {sanitizeHandle(post.author.handle, '@')}
-                </NewText>
+                </Text>
               </Link>
             </View>
             {currentAccount?.did !== post.author.did && (
@@ -393,48 +398,48 @@ let PostThreadItemLoaded = ({
                 ]}>
                 {post.repostCount != null && post.repostCount !== 0 ? (
                   <Link href={repostsHref} title={repostsTitle}>
-                    <NewText
+                    <Text
                       testID="repostCount-expanded"
                       style={[a.text_md, t.atoms.text_contrast_medium]}>
-                      <NewText style={[a.text_md, a.font_bold, t.atoms.text]}>
+                      <Text style={[a.text_md, a.font_bold, t.atoms.text]}>
                         {formatCount(i18n, post.repostCount)}
-                      </NewText>{' '}
+                      </Text>{' '}
                       <Plural
                         value={post.repostCount}
                         one="repost"
                         other="reposts"
                       />
-                    </NewText>
+                    </Text>
                   </Link>
                 ) : null}
                 {post.quoteCount != null &&
                 post.quoteCount !== 0 &&
                 !post.viewer?.embeddingDisabled ? (
                   <Link href={quotesHref} title={quotesTitle}>
-                    <NewText
+                    <Text
                       testID="quoteCount-expanded"
                       style={[a.text_md, t.atoms.text_contrast_medium]}>
-                      <NewText style={[a.text_md, a.font_bold, t.atoms.text]}>
+                      <Text style={[a.text_md, a.font_bold, t.atoms.text]}>
                         {formatCount(i18n, post.quoteCount)}
-                      </NewText>{' '}
+                      </Text>{' '}
                       <Plural
                         value={post.quoteCount}
                         one="quote"
                         other="quotes"
                       />
-                    </NewText>
+                    </Text>
                   </Link>
                 ) : null}
                 {post.likeCount != null && post.likeCount !== 0 ? (
                   <Link href={likesHref} title={likesTitle}>
-                    <NewText
+                    <Text
                       testID="likeCount-expanded"
                       style={[a.text_md, t.atoms.text_contrast_medium]}>
-                      <NewText style={[a.text_md, a.font_bold, t.atoms.text]}>
+                      <Text style={[a.text_md, a.font_bold, t.atoms.text]}>
                         {formatCount(i18n, post.likeCount)}
-                      </NewText>{' '}
+                      </Text>{' '}
                       <Plural value={post.likeCount} one="like" other="likes" />
-                    </NewText>
+                    </Text>
                   </Link>
                 ) : null}
               </View>
@@ -617,13 +622,13 @@ let PostThreadItemLoaded = ({
               href={postHref}
               title={itemTitle}
               noFeedback>
-              <Text type="sm-medium" style={pal.textLight}>
+              <Text
+                style={[t.atoms.text_contrast_medium, a.font_bold, a.text_sm]}>
                 <Trans>More</Trans>
               </Text>
-              <FontAwesomeIcon
-                icon="angle-right"
-                color={pal.colors.textLight}
-                size={14}
+              <ChevronRightIcon
+                size="xs"
+                style={[t.atoms.text_contrast_medium]}
               />
             </Link>
           ) : undefined}
@@ -732,32 +737,124 @@ function ExpandedPostDetails({
   }, [openLink, translatorUrl])
 
   return (
-    <View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_sm, a.pt_md]}>
-      <NewText style={[a.text_sm, t.atoms.text_contrast_medium]}>
-        {niceDate(i18n, post.indexedAt)}
-      </NewText>
-      {isRootPost && (
-        <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
-      )}
-      {needsTranslation && (
-        <>
-          <NewText style={[a.text_sm, t.atoms.text_contrast_medium]}>
-            &middot;
-          </NewText>
+    <View style={[a.gap_md, a.pt_md, a.align_start]}>
+      <BackdatedPostIndicator post={post} />
+      <View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_sm]}>
+        <Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
+          {niceDate(i18n, post.indexedAt)}
+        </Text>
+        {isRootPost && (
+          <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
+        )}
+        {needsTranslation && (
+          <>
+            <Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
+              &middot;
+            </Text>
 
-          <InlineLinkText
-            to="#"
-            label={_(msg`Translate`)}
-            style={[a.text_sm, pal.link]}
-            onPress={onTranslatePress}>
-            <Trans>Translate</Trans>
-          </InlineLinkText>
-        </>
-      )}
+            <InlineLinkText
+              to="#"
+              label={_(msg`Translate`)}
+              style={[a.text_sm, pal.link]}
+              onPress={onTranslatePress}>
+              <Trans>Translate</Trans>
+            </InlineLinkText>
+          </>
+        )}
+      </View>
     </View>
   )
 }
 
+function BackdatedPostIndicator({post}: {post: AppBskyFeedDefs.PostView}) {
+  const t = useTheme()
+  const {_, i18n} = useLingui()
+  const control = Prompt.usePromptControl()
+
+  const indexedAt = new Date(post.indexedAt)
+  const createdAt = AppBskyFeedPost.isRecord(post.record)
+    ? new Date(post.record.createdAt)
+    : new Date(post.indexedAt)
+
+  // backdated if createdAt is 24 hours or more before indexedAt
+  const isBackdated =
+    indexedAt.getTime() - createdAt.getTime() > 24 * 60 * 60 * 1000
+
+  if (!isBackdated) return null
+
+  const orange = t.name === 'light' ? colors.warning.dark : colors.warning.light
+
+  return (
+    <>
+      <Button
+        label={_(msg`Archived post`)}
+        accessibilityHint={_(
+          msg`Show information about when this post was created`,
+        )}
+        onPress={e => {
+          e.preventDefault()
+          e.stopPropagation()
+          control.open()
+        }}>
+        {({hovered, pressed}) => (
+          <View
+            style={[
+              a.flex_row,
+              a.align_center,
+              a.rounded_full,
+              t.atoms.bg_contrast_25,
+              (hovered || pressed) && t.atoms.bg_contrast_50,
+              {
+                gap: 3,
+                paddingHorizontal: 6,
+                paddingVertical: 3,
+              },
+            ]}>
+            <CalendarClockIcon fill={orange} size="sm" aria-hidden />
+            <Text
+              style={[
+                a.text_xs,
+                a.font_bold,
+                a.leading_tight,
+                t.atoms.text_contrast_medium,
+              ]}>
+              <Trans>Archived from {niceDate(i18n, createdAt)}</Trans>
+            </Text>
+          </View>
+        )}
+      </Button>
+
+      <Prompt.Outer control={control}>
+        <Prompt.TitleText>
+          <Trans>Archived post</Trans>
+        </Prompt.TitleText>
+        <Prompt.DescriptionText>
+          <Trans>
+            This post claims to have been created on{' '}
+            <RNText style={[a.font_bold]}>{niceDate(i18n, createdAt)}</RNText>,
+            but was first seen by Bluesky on{' '}
+            <RNText style={[a.font_bold]}>{niceDate(i18n, indexedAt)}</RNText>.
+          </Trans>
+        </Prompt.DescriptionText>
+        <Text
+          style={[
+            a.text_md,
+            a.leading_snug,
+            t.atoms.text_contrast_high,
+            a.pb_xl,
+          ]}>
+          <Trans>
+            Bluesky cannot confirm the authenticity of the claimed date.
+          </Trans>
+        </Text>
+        <Prompt.Actions>
+          <Prompt.Action cta={_(msg`Okay`)} onPress={() => {}} />
+        </Prompt.Actions>
+      </Prompt.Outer>
+    </>
+  )
+}
+
 function getThreadAuthor(
   post: AppBskyFeedDefs.PostView,
   record: AppBskyFeedPost.Record,
-- 
cgit 1.4.1

