Bluesky app fork with some witchin' additions 💫

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

authored by samuel.fm and committed by

GitHub 6471e809 f2916ce5

+171 -68
+1
assets/icons/calendarClock_stroke2_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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" clip-rule="evenodd"/></svg>
+5
src/components/icons/CalendarClock.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const CalendarClock_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 + 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', 5 + })
+165 -68
src/view/com/post-thread/PostThreadItem.tsx
··· 1 1 import React, {memo, useMemo} from 'react' 2 - import {StyleSheet, View} from 'react-native' 2 + import {StyleSheet, Text as RNText, View} from 'react-native' 3 3 import { 4 4 AppBskyFeedDefs, 5 5 AppBskyFeedPost, ··· 8 8 ModerationDecision, 9 9 RichText as RichTextAPI, 10 10 } from '@atproto/api' 11 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 12 11 import {msg, Plural, Trans} from '@lingui/macro' 13 12 import {useLingui} from '@lingui/react' 14 13 ··· 21 20 import {countLines} from '#/lib/strings/helpers' 22 21 import {niceDate} from '#/lib/strings/time' 23 22 import {s} from '#/lib/styles' 23 + import {getTranslatorLink, isPostInLanguage} from '#/locale/helpers' 24 24 import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' 25 25 import {useLanguagePrefs} from '#/state/preferences' 26 26 import {ThreadPost} from '#/state/queries/post-thread' ··· 28 28 import {useComposerControls} from '#/state/shell/composer' 29 29 import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' 30 30 import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' 31 + import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 32 + import {Link, TextLink} from '#/view/com/util/Link' 33 + import {formatCount} from '#/view/com/util/numeric/format' 34 + import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls' 35 + import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds' 36 + import {PostMeta} from '#/view/com/util/PostMeta' 37 + import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 31 38 import {atoms as a, useTheme} from '#/alf' 39 + import {colors} from '#/components/Admonition' 40 + import {Button} from '#/components/Button' 41 + import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock' 42 + import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron' 43 + import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' 32 44 import {InlineLinkText} from '#/components/Link' 45 + import {ContentHider} from '#/components/moderation/ContentHider' 46 + import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' 47 + import {PostAlerts} from '#/components/moderation/PostAlerts' 48 + import {PostHider} from '#/components/moderation/PostHider' 33 49 import {AppModerationCause} from '#/components/Pills' 50 + import * as Prompt from '#/components/Prompt' 34 51 import {RichText} from '#/components/RichText' 35 52 import {SubtleWebHover} from '#/components/SubtleWebHover' 36 - import {Text as NewText} from '#/components/Typography' 37 - import {ContentHider} from '../../../components/moderation/ContentHider' 38 - import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe' 39 - import {PostAlerts} from '../../../components/moderation/PostAlerts' 40 - import {PostHider} from '../../../components/moderation/PostHider' 41 - import {WhoCanReply} from '../../../components/WhoCanReply' 42 - import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers' 43 - import {ErrorMessage} from '../util/error/ErrorMessage' 44 - import {Link, TextLink} from '../util/Link' 45 - import {formatCount} from '../util/numeric/format' 46 - import {PostCtrls} from '../util/post-ctrls/PostCtrls' 47 - import {PostEmbeds, PostEmbedViewContext} from '../util/post-embeds' 48 - import {PostMeta} from '../util/PostMeta' 49 - import {Text} from '../util/text/Text' 50 - import {PreviewableUserAvatar} from '../util/UserAvatar' 53 + import {Text} from '#/components/Typography' 54 + import {WhoCanReply} from '#/components/WhoCanReply' 51 55 52 56 export function PostThreadItem({ 53 57 post, ··· 125 129 } 126 130 127 131 function PostThreadItemDeleted({hideTopBorder}: {hideTopBorder?: boolean}) { 128 - const pal = usePalette('default') 132 + const t = useTheme() 129 133 return ( 130 134 <View 131 135 style={[ 132 - styles.outer, 133 - pal.border, 134 - pal.view, 135 - s.p20, 136 - s.flexRow, 137 - hideTopBorder && styles.noTopBorder, 136 + t.atoms.bg, 137 + t.atoms.border_contrast_low, 138 + a.p_xl, 139 + a.pl_lg, 140 + a.flex_row, 141 + a.gap_md, 142 + !hideTopBorder && a.border_t, 138 143 ]}> 139 - <FontAwesomeIcon icon={['far', 'trash-can']} color={pal.colors.icon} /> 140 - <Text style={[pal.textLight, s.ml10]}> 144 + <TrashIcon style={[t.atoms.text]} /> 145 + <Text style={[t.atoms.text_contrast_medium, a.mt_2xs]}> 141 146 <Trans>This post has been deleted.</Trans> 142 147 </Text> 143 148 </View> ··· 308 313 /> 309 314 <View style={[a.flex_1]}> 310 315 <Link style={s.flex1} href={authorHref} title={authorTitle}> 311 - <NewText 316 + <Text 312 317 emoji 313 318 style={[a.text_lg, a.font_bold, a.leading_snug, a.self_start]} 314 319 numberOfLines={1}> ··· 317 322 sanitizeHandle(post.author.handle), 318 323 moderation.ui('displayName'), 319 324 )} 320 - </NewText> 325 + </Text> 321 326 </Link> 322 327 <Link style={s.flex1} href={authorHref} title={authorTitle}> 323 - <NewText 328 + <Text 324 329 emoji 325 330 style={[ 326 331 a.text_md, ··· 329 334 ]} 330 335 numberOfLines={1}> 331 336 {sanitizeHandle(post.author.handle, '@')} 332 - </NewText> 337 + </Text> 333 338 </Link> 334 339 </View> 335 340 {currentAccount?.did !== post.author.did && ( ··· 393 398 ]}> 394 399 {post.repostCount != null && post.repostCount !== 0 ? ( 395 400 <Link href={repostsHref} title={repostsTitle}> 396 - <NewText 401 + <Text 397 402 testID="repostCount-expanded" 398 403 style={[a.text_md, t.atoms.text_contrast_medium]}> 399 - <NewText style={[a.text_md, a.font_bold, t.atoms.text]}> 404 + <Text style={[a.text_md, a.font_bold, t.atoms.text]}> 400 405 {formatCount(i18n, post.repostCount)} 401 - </NewText>{' '} 406 + </Text>{' '} 402 407 <Plural 403 408 value={post.repostCount} 404 409 one="repost" 405 410 other="reposts" 406 411 /> 407 - </NewText> 412 + </Text> 408 413 </Link> 409 414 ) : null} 410 415 {post.quoteCount != null && 411 416 post.quoteCount !== 0 && 412 417 !post.viewer?.embeddingDisabled ? ( 413 418 <Link href={quotesHref} title={quotesTitle}> 414 - <NewText 419 + <Text 415 420 testID="quoteCount-expanded" 416 421 style={[a.text_md, t.atoms.text_contrast_medium]}> 417 - <NewText style={[a.text_md, a.font_bold, t.atoms.text]}> 422 + <Text style={[a.text_md, a.font_bold, t.atoms.text]}> 418 423 {formatCount(i18n, post.quoteCount)} 419 - </NewText>{' '} 424 + </Text>{' '} 420 425 <Plural 421 426 value={post.quoteCount} 422 427 one="quote" 423 428 other="quotes" 424 429 /> 425 - </NewText> 430 + </Text> 426 431 </Link> 427 432 ) : null} 428 433 {post.likeCount != null && post.likeCount !== 0 ? ( 429 434 <Link href={likesHref} title={likesTitle}> 430 - <NewText 435 + <Text 431 436 testID="likeCount-expanded" 432 437 style={[a.text_md, t.atoms.text_contrast_medium]}> 433 - <NewText style={[a.text_md, a.font_bold, t.atoms.text]}> 438 + <Text style={[a.text_md, a.font_bold, t.atoms.text]}> 434 439 {formatCount(i18n, post.likeCount)} 435 - </NewText>{' '} 440 + </Text>{' '} 436 441 <Plural value={post.likeCount} one="like" other="likes" /> 437 - </NewText> 442 + </Text> 438 443 </Link> 439 444 ) : null} 440 445 </View> ··· 617 622 href={postHref} 618 623 title={itemTitle} 619 624 noFeedback> 620 - <Text type="sm-medium" style={pal.textLight}> 625 + <Text 626 + style={[t.atoms.text_contrast_medium, a.font_bold, a.text_sm]}> 621 627 <Trans>More</Trans> 622 628 </Text> 623 - <FontAwesomeIcon 624 - icon="angle-right" 625 - color={pal.colors.textLight} 626 - size={14} 629 + <ChevronRightIcon 630 + size="xs" 631 + style={[t.atoms.text_contrast_medium]} 627 632 /> 628 633 </Link> 629 634 ) : undefined} ··· 732 737 }, [openLink, translatorUrl]) 733 738 734 739 return ( 735 - <View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_sm, a.pt_md]}> 736 - <NewText style={[a.text_sm, t.atoms.text_contrast_medium]}> 737 - {niceDate(i18n, post.indexedAt)} 738 - </NewText> 739 - {isRootPost && ( 740 - <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} /> 741 - )} 742 - {needsTranslation && ( 743 - <> 744 - <NewText style={[a.text_sm, t.atoms.text_contrast_medium]}> 745 - &middot; 746 - </NewText> 740 + <View style={[a.gap_md, a.pt_md, a.align_start]}> 741 + <BackdatedPostIndicator post={post} /> 742 + <View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_sm]}> 743 + <Text style={[a.text_sm, t.atoms.text_contrast_medium]}> 744 + {niceDate(i18n, post.indexedAt)} 745 + </Text> 746 + {isRootPost && ( 747 + <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} /> 748 + )} 749 + {needsTranslation && ( 750 + <> 751 + <Text style={[a.text_sm, t.atoms.text_contrast_medium]}> 752 + &middot; 753 + </Text> 747 754 748 - <InlineLinkText 749 - to="#" 750 - label={_(msg`Translate`)} 751 - style={[a.text_sm, pal.link]} 752 - onPress={onTranslatePress}> 753 - <Trans>Translate</Trans> 754 - </InlineLinkText> 755 - </> 756 - )} 755 + <InlineLinkText 756 + to="#" 757 + label={_(msg`Translate`)} 758 + style={[a.text_sm, pal.link]} 759 + onPress={onTranslatePress}> 760 + <Trans>Translate</Trans> 761 + </InlineLinkText> 762 + </> 763 + )} 764 + </View> 757 765 </View> 766 + ) 767 + } 768 + 769 + function BackdatedPostIndicator({post}: {post: AppBskyFeedDefs.PostView}) { 770 + const t = useTheme() 771 + const {_, i18n} = useLingui() 772 + const control = Prompt.usePromptControl() 773 + 774 + const indexedAt = new Date(post.indexedAt) 775 + const createdAt = AppBskyFeedPost.isRecord(post.record) 776 + ? new Date(post.record.createdAt) 777 + : new Date(post.indexedAt) 778 + 779 + // backdated if createdAt is 24 hours or more before indexedAt 780 + const isBackdated = 781 + indexedAt.getTime() - createdAt.getTime() > 24 * 60 * 60 * 1000 782 + 783 + if (!isBackdated) return null 784 + 785 + const orange = t.name === 'light' ? colors.warning.dark : colors.warning.light 786 + 787 + return ( 788 + <> 789 + <Button 790 + label={_(msg`Archived post`)} 791 + accessibilityHint={_( 792 + msg`Show information about when this post was created`, 793 + )} 794 + onPress={e => { 795 + e.preventDefault() 796 + e.stopPropagation() 797 + control.open() 798 + }}> 799 + {({hovered, pressed}) => ( 800 + <View 801 + style={[ 802 + a.flex_row, 803 + a.align_center, 804 + a.rounded_full, 805 + t.atoms.bg_contrast_25, 806 + (hovered || pressed) && t.atoms.bg_contrast_50, 807 + { 808 + gap: 3, 809 + paddingHorizontal: 6, 810 + paddingVertical: 3, 811 + }, 812 + ]}> 813 + <CalendarClockIcon fill={orange} size="sm" aria-hidden /> 814 + <Text 815 + style={[ 816 + a.text_xs, 817 + a.font_bold, 818 + a.leading_tight, 819 + t.atoms.text_contrast_medium, 820 + ]}> 821 + <Trans>Archived from {niceDate(i18n, createdAt)}</Trans> 822 + </Text> 823 + </View> 824 + )} 825 + </Button> 826 + 827 + <Prompt.Outer control={control}> 828 + <Prompt.TitleText> 829 + <Trans>Archived post</Trans> 830 + </Prompt.TitleText> 831 + <Prompt.DescriptionText> 832 + <Trans> 833 + This post claims to have been created on{' '} 834 + <RNText style={[a.font_bold]}>{niceDate(i18n, createdAt)}</RNText>, 835 + but was first seen by Bluesky on{' '} 836 + <RNText style={[a.font_bold]}>{niceDate(i18n, indexedAt)}</RNText>. 837 + </Trans> 838 + </Prompt.DescriptionText> 839 + <Text 840 + style={[ 841 + a.text_md, 842 + a.leading_snug, 843 + t.atoms.text_contrast_high, 844 + a.pb_xl, 845 + ]}> 846 + <Trans> 847 + Bluesky cannot confirm the authenticity of the claimed date. 848 + </Trans> 849 + </Text> 850 + <Prompt.Actions> 851 + <Prompt.Action cta={_(msg`Okay`)} onPress={() => {}} /> 852 + </Prompt.Actions> 853 + </Prompt.Outer> 854 + </> 758 855 ) 759 856 } 760 857