Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add backdated post indicator #6156

Merged
merged 7 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions assets/icons/calendarClock_stroke2_corner0_rounded.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/components/icons/CalendarClock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'

export const CalendarClock_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M15.439 3.148a1 1 0 0 1 .41.645l.568 3.22a7 7 0 1 1-6.174 10.97L4.32 19.027a1 1 0 0 1-1.159-.811L1.078 6.398a1 1 0 0 1 .81-1.158l12.803-2.258a1 1 0 0 1 .748.166ZM9.325 16.114A7 7 0 0 1 9 14c0-1.56.51-3 1.372-4.164l-6.456 1.139 1.041 5.909 4.368-.77ZM3.568 9.005l10.833-1.91-.347-1.97L3.22 7.036l.347 1.97ZM16 9a5 5 0 1 0 0 10 5 5 0 0 0 0-10Zm0 2a1 1 0 0 1 1 1v1.586l1.374 1.374a1 1 0 0 1-1.414 1.414l-1.667-1.667A1 1 0 0 1 15 14v-2a1 1 0 0 1 1-1Z',
})
233 changes: 165 additions & 68 deletions src/view/com/post-thread/PostThreadItem.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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'

Expand All @@ -21,33 +20,38 @@ 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'
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,
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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}>
Expand All @@ -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,
Expand All @@ -329,7 +334,7 @@ let PostThreadItemLoaded = ({
]}
numberOfLines={1}>
{sanitizeHandle(post.author.handle, '@')}
</NewText>
</Text>
</Link>
</View>
{currentAccount?.did !== post.author.did && (
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small q, why do we use RNText here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nested styles

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,
Expand Down
Loading